killbill-aplcache

payment: add plugin to process external payments This new

7/27/2012 5:09:12 PM

Details

diff --git a/api/src/main/java/com/ning/billing/payment/api/PaymentMethodPlugin.java b/api/src/main/java/com/ning/billing/payment/api/PaymentMethodPlugin.java
index cacdf3a..b0151ae 100644
--- a/api/src/main/java/com/ning/billing/payment/api/PaymentMethodPlugin.java
+++ b/api/src/main/java/com/ning/billing/payment/api/PaymentMethodPlugin.java
@@ -13,6 +13,7 @@
  * License for the specific language governing permissions and limitations
  * under the License.
  */
+
 package com.ning.billing.payment.api;
 
 import java.util.List;
@@ -28,12 +29,12 @@ public interface PaymentMethodPlugin {
     public String getValueString(String key);
 
     public class PaymentMethodKVInfo {
+
         private final String key;
         private final Object value;
         private final Boolean isUpdatable;
 
         public PaymentMethodKVInfo(final String key, final Object value, final Boolean isUpdatable) {
-            super();
             this.key = key;
             this.value = value;
             this.isUpdatable = isUpdatable;
@@ -50,5 +51,48 @@ public interface PaymentMethodPlugin {
         public Boolean getIsUpdatable() {
             return isUpdatable;
         }
+
+        @Override
+        public String toString() {
+            final StringBuilder sb = new StringBuilder();
+            sb.append("PaymentMethodKVInfo");
+            sb.append("{key='").append(key).append('\'');
+            sb.append(", value=").append(value);
+            sb.append(", isUpdatable=").append(isUpdatable);
+            sb.append('}');
+            return sb.toString();
+        }
+
+        @Override
+        public boolean equals(final Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+
+            final PaymentMethodKVInfo that = (PaymentMethodKVInfo) o;
+
+            if (isUpdatable != null ? !isUpdatable.equals(that.isUpdatable) : that.isUpdatable != null) {
+                return false;
+            }
+            if (key != null ? !key.equals(that.key) : that.key != null) {
+                return false;
+            }
+            if (value != null ? !value.equals(that.value) : that.value != null) {
+                return false;
+            }
+
+            return true;
+        }
+
+        @Override
+        public int hashCode() {
+            int result = key != null ? key.hashCode() : 0;
+            result = 31 * result + (value != null ? value.hashCode() : 0);
+            result = 31 * result + (isUpdatable != null ? isUpdatable.hashCode() : 0);
+            return result;
+        }
     }
 }
diff --git a/api/src/main/java/com/ning/billing/payment/plugin/api/PaymentPluginApi.java b/api/src/main/java/com/ning/billing/payment/plugin/api/PaymentPluginApi.java
index 5f8fe2d..dd54118 100644
--- a/api/src/main/java/com/ning/billing/payment/plugin/api/PaymentPluginApi.java
+++ b/api/src/main/java/com/ning/billing/payment/plugin/api/PaymentPluginApi.java
@@ -33,11 +33,11 @@ public interface PaymentPluginApi {
     public PaymentInfoPlugin getPaymentInfo(UUID paymentId)
             throws PaymentPluginApiException;
 
-    public void processRefund(Account account, UUID paymentId, BigDecimal refundAmout)
-    throws PaymentPluginApiException;
+    public void processRefund(final Account account, final UUID paymentId, BigDecimal refundAmount)
+            throws PaymentPluginApiException;
 
     public int getNbRefundForPaymentAmount(final Account account, final UUID paymentId, final BigDecimal refundAmount)
-        throws PaymentPluginApiException;
+            throws PaymentPluginApiException;
 
     public String createPaymentProviderAccount(Account account)
             throws PaymentPluginApiException;
diff --git a/payment/src/main/java/com/ning/billing/payment/glue/DefaultPaymentProviderPluginRegistryProvider.java b/payment/src/main/java/com/ning/billing/payment/glue/DefaultPaymentProviderPluginRegistryProvider.java
new file mode 100644
index 0000000..5a92d7f
--- /dev/null
+++ b/payment/src/main/java/com/ning/billing/payment/glue/DefaultPaymentProviderPluginRegistryProvider.java
@@ -0,0 +1,46 @@
+/*
+ * 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.payment.glue;
+
+import com.ning.billing.config.PaymentConfig;
+import com.ning.billing.payment.provider.DefaultPaymentProviderPluginRegistry;
+import com.ning.billing.payment.provider.ExternalPaymentProviderPlugin;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class DefaultPaymentProviderPluginRegistryProvider implements Provider<DefaultPaymentProviderPluginRegistry> {
+
+    private final PaymentConfig paymentConfig;
+    private final ExternalPaymentProviderPlugin externalPaymentProviderPlugin;
+
+    @Inject
+    public DefaultPaymentProviderPluginRegistryProvider(final PaymentConfig paymentConfig, final ExternalPaymentProviderPlugin externalPaymentProviderPlugin) {
+        this.paymentConfig = paymentConfig;
+        this.externalPaymentProviderPlugin = externalPaymentProviderPlugin;
+    }
+
+    @Override
+    public DefaultPaymentProviderPluginRegistry get() {
+        final DefaultPaymentProviderPluginRegistry pluginRegistry = new DefaultPaymentProviderPluginRegistry(paymentConfig);
+
+        // Make the external payment provider plugin available by default
+        pluginRegistry.register(externalPaymentProviderPlugin, ExternalPaymentProviderPlugin.PLUGIN_NAME);
+
+        return pluginRegistry;
+    }
+}
diff --git a/payment/src/main/java/com/ning/billing/payment/glue/PaymentModule.java b/payment/src/main/java/com/ning/billing/payment/glue/PaymentModule.java
index 348a0ed..9e601aa 100644
--- a/payment/src/main/java/com/ning/billing/payment/glue/PaymentModule.java
+++ b/payment/src/main/java/com/ning/billing/payment/glue/PaymentModule.java
@@ -25,9 +25,6 @@ import org.skife.config.ConfigSource;
 import org.skife.config.ConfigurationObjectFactory;
 import org.skife.config.SimplePropertyConfigSource;
 
-import com.google.common.annotations.VisibleForTesting;
-import com.google.inject.AbstractModule;
-import com.google.inject.name.Names;
 import com.ning.billing.config.PaymentConfig;
 import com.ning.billing.payment.api.DefaultPaymentApi;
 import com.ning.billing.payment.api.PaymentApi;
@@ -39,7 +36,6 @@ import com.ning.billing.payment.core.PaymentProcessor;
 import com.ning.billing.payment.core.RefundProcessor;
 import com.ning.billing.payment.dao.AuditedPaymentDao;
 import com.ning.billing.payment.dao.PaymentDao;
-import com.ning.billing.payment.provider.DefaultPaymentProviderPluginRegistry;
 import com.ning.billing.payment.provider.PaymentProviderPluginRegistry;
 import com.ning.billing.payment.retry.AutoPayRetryService;
 import com.ning.billing.payment.retry.AutoPayRetryService.AutoPayRetryServiceScheduler;
@@ -48,6 +44,10 @@ import com.ning.billing.payment.retry.FailedPaymentRetryService.FailedPaymentRet
 import com.ning.billing.payment.retry.PluginFailureRetryService;
 import com.ning.billing.payment.retry.PluginFailureRetryService.PluginFailureRetryServiceScheduler;
 
+import com.google.common.annotations.VisibleForTesting;
+import com.google.inject.AbstractModule;
+import com.google.inject.name.Names;
+
 public class PaymentModule extends AbstractModule {
     private static final int PLUGIN_NB_THREADS = 3;
     private static final String PLUGIN_THREAD_PREFIX = "Plugin-th-";
@@ -107,7 +107,8 @@ public class PaymentModule extends AbstractModule {
         final PaymentConfig paymentConfig = factory.build(PaymentConfig.class);
 
         bind(PaymentConfig.class).toInstance(paymentConfig);
-        bind(PaymentProviderPluginRegistry.class).to(DefaultPaymentProviderPluginRegistry.class).asEagerSingleton();
+        bind(PaymentProviderPluginRegistry.class).toProvider(DefaultPaymentProviderPluginRegistryProvider.class).asEagerSingleton();
+
         bind(PaymentApi.class).to(DefaultPaymentApi.class).asEagerSingleton();
         bind(InvoiceHandler.class).asEagerSingleton();
         bind(TagHandler.class).asEagerSingleton();
diff --git a/payment/src/main/java/com/ning/billing/payment/provider/DefaultNoOpPaymentInfoPlugin.java b/payment/src/main/java/com/ning/billing/payment/provider/DefaultNoOpPaymentInfoPlugin.java
index 3dd0a01..135ba1b 100644
--- a/payment/src/main/java/com/ning/billing/payment/provider/DefaultNoOpPaymentInfoPlugin.java
+++ b/payment/src/main/java/com/ning/billing/payment/provider/DefaultNoOpPaymentInfoPlugin.java
@@ -13,6 +13,7 @@
  * License for the specific language governing permissions and limitations
  * under the License.
  */
+
 package com.ning.billing.payment.provider;
 
 import java.math.BigDecimal;
@@ -31,7 +32,6 @@ public class DefaultNoOpPaymentInfoPlugin implements PaymentInfoPlugin {
 
     public DefaultNoOpPaymentInfoPlugin(final BigDecimal amount, final DateTime effectiveDate,
                                         final DateTime createdDate, final PaymentPluginStatus status, final String error) {
-        super();
         this.amount = amount;
         this.effectiveDate = effectiveDate;
         this.createdDate = createdDate;
@@ -39,13 +39,11 @@ public class DefaultNoOpPaymentInfoPlugin implements PaymentInfoPlugin {
         this.error = error;
     }
 
-
     @Override
     public BigDecimal getAmount() {
         return amount;
     }
 
-
     @Override
     public DateTime getEffectiveDate() {
         return effectiveDate;
@@ -71,15 +69,66 @@ public class DefaultNoOpPaymentInfoPlugin implements PaymentInfoPlugin {
         return null;
     }
 
-
     @Override
     public String getExtFirstReferenceId() {
         return null;
     }
 
-
     @Override
     public String getExtSecondReferenceId() {
         return null;
     }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("DefaultNoOpPaymentInfoPlugin");
+        sb.append("{amount=").append(amount);
+        sb.append(", effectiveDate=").append(effectiveDate);
+        sb.append(", createdDate=").append(createdDate);
+        sb.append(", status=").append(status);
+        sb.append(", error='").append(error).append('\'');
+        sb.append('}');
+        return sb.toString();
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final DefaultNoOpPaymentInfoPlugin that = (DefaultNoOpPaymentInfoPlugin) o;
+
+        if (amount != null ? !amount.equals(that.amount) : that.amount != null) {
+            return false;
+        }
+        if (createdDate != null ? !createdDate.equals(that.createdDate) : that.createdDate != null) {
+            return false;
+        }
+        if (effectiveDate != null ? !effectiveDate.equals(that.effectiveDate) : that.effectiveDate != null) {
+            return false;
+        }
+        if (error != null ? !error.equals(that.error) : that.error != null) {
+            return false;
+        }
+        if (status != that.status) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = amount != null ? amount.hashCode() : 0;
+        result = 31 * result + (effectiveDate != null ? effectiveDate.hashCode() : 0);
+        result = 31 * result + (createdDate != null ? createdDate.hashCode() : 0);
+        result = 31 * result + (status != null ? status.hashCode() : 0);
+        result = 31 * result + (error != null ? error.hashCode() : 0);
+        return result;
+    }
 }
diff --git a/payment/src/main/java/com/ning/billing/payment/provider/DefaultNoOpPaymentMethodPlugin.java b/payment/src/main/java/com/ning/billing/payment/provider/DefaultNoOpPaymentMethodPlugin.java
index af88c29..f84b111 100644
--- a/payment/src/main/java/com/ning/billing/payment/provider/DefaultNoOpPaymentMethodPlugin.java
+++ b/payment/src/main/java/com/ning/billing/payment/provider/DefaultNoOpPaymentMethodPlugin.java
@@ -13,6 +13,7 @@
  * License for the specific language governing permissions and limitations
  * under the License.
  */
+
 package com.ning.billing.payment.provider;
 
 import java.util.List;
@@ -22,8 +23,8 @@ import com.ning.billing.payment.api.PaymentMethodPlugin;
 
 public class DefaultNoOpPaymentMethodPlugin implements PaymentMethodPlugin {
 
-    private String externalId;
-    private boolean isDefault;
+    private final String externalId;
+    private final boolean isDefault;
     private List<PaymentMethodKVInfo> props;
 
     public DefaultNoOpPaymentMethodPlugin(final PaymentMethodPlugin src) {
@@ -34,7 +35,6 @@ public class DefaultNoOpPaymentMethodPlugin implements PaymentMethodPlugin {
 
     public DefaultNoOpPaymentMethodPlugin(final String externalId, final boolean isDefault,
                                           final List<PaymentMethodKVInfo> props) {
-        super();
         this.externalId = externalId;
         this.isDefault = isDefault;
         this.props = props;
@@ -55,14 +55,6 @@ public class DefaultNoOpPaymentMethodPlugin implements PaymentMethodPlugin {
         return props;
     }
 
-    public void setExternalId(final String externalId) {
-        this.externalId = externalId;
-    }
-
-    public void setDefault(final boolean isDefault) {
-        this.isDefault = isDefault;
-    }
-
     public void setProps(final List<PaymentMethodKVInfo> props) {
         this.props = props;
     }
@@ -72,11 +64,56 @@ public class DefaultNoOpPaymentMethodPlugin implements PaymentMethodPlugin {
         if (props == null) {
             return null;
         }
+
         for (final PaymentMethodKVInfo cur : props) {
             if (cur.getKey().equals(key)) {
                 return cur.getValue().toString();
             }
         }
+
         return null;
     }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("DefaultNoOpPaymentMethodPlugin");
+        sb.append("{externalId='").append(externalId).append('\'');
+        sb.append(", isDefault=").append(isDefault);
+        sb.append(", props=").append(props);
+        sb.append('}');
+        return sb.toString();
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final DefaultNoOpPaymentMethodPlugin that = (DefaultNoOpPaymentMethodPlugin) o;
+
+        if (isDefault != that.isDefault) {
+            return false;
+        }
+        if (externalId != null ? !externalId.equals(that.externalId) : that.externalId != null) {
+            return false;
+        }
+        if (props != null ? !props.equals(that.props) : that.props != null) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = externalId != null ? externalId.hashCode() : 0;
+        result = 31 * result + (isDefault ? 1 : 0);
+        result = 31 * result + (props != null ? props.hashCode() : 0);
+        return result;
+    }
 }
diff --git a/payment/src/main/java/com/ning/billing/payment/provider/DefaultNoOpPaymentProviderPlugin.java b/payment/src/main/java/com/ning/billing/payment/provider/DefaultNoOpPaymentProviderPlugin.java
index c43b64b..2db4092 100644
--- a/payment/src/main/java/com/ning/billing/payment/provider/DefaultNoOpPaymentProviderPlugin.java
+++ b/payment/src/main/java/com/ning/billing/payment/provider/DefaultNoOpPaymentProviderPlugin.java
@@ -24,7 +24,6 @@ import java.util.UUID;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicBoolean;
 
-import com.google.inject.Inject;
 import com.ning.billing.account.api.Account;
 import com.ning.billing.payment.api.PaymentMethodPlugin;
 import com.ning.billing.payment.plugin.api.NoOpPaymentPluginApi;
@@ -34,15 +33,24 @@ import com.ning.billing.payment.plugin.api.PaymentPluginApiException;
 import com.ning.billing.payment.plugin.api.PaymentProviderAccount;
 import com.ning.billing.util.clock.Clock;
 
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.Multimap;
+import com.google.inject.Inject;
+
 public class DefaultNoOpPaymentProviderPlugin implements NoOpPaymentPluginApi {
+
+    private static final String PLUGIN_NAME = "__NO_OP__";
+
     private final AtomicBoolean makeNextInvoiceFailWithError = new AtomicBoolean(false);
     private final AtomicBoolean makeNextInvoiceFailWithException = new AtomicBoolean(false);
     private final AtomicBoolean makeAllInvoicesFailWithError = new AtomicBoolean(false);
-    private final Map<UUID, PaymentInfoPlugin> payments = new ConcurrentHashMap<UUID, PaymentInfoPlugin>();
 
+    private final Map<UUID, PaymentInfoPlugin> payments = new ConcurrentHashMap<UUID, PaymentInfoPlugin>();
+    // Note: we can't use HashMultiMap as we care about storing duplicate key/value pairs
+    private final Multimap<UUID, BigDecimal> refunds = LinkedListMultimap.<UUID, BigDecimal>create();
     private final Map<String, List<PaymentMethodPlugin>> paymentMethods = new ConcurrentHashMap<String, List<PaymentMethodPlugin>>();
-
     private final Map<String, PaymentProviderAccount> accounts = new ConcurrentHashMap<String, PaymentProviderAccount>();
+
     private final Clock clock;
 
     @Inject
@@ -73,13 +81,11 @@ public class DefaultNoOpPaymentProviderPlugin implements NoOpPaymentPluginApi {
         makeAllInvoicesFailWithError.set(failure);
     }
 
-
     @Override
     public String getName() {
-        return null;
+        return PLUGIN_NAME;
     }
 
-
     @Override
     public PaymentInfoPlugin processPayment(final String externalKey, final UUID paymentId, final BigDecimal amount) throws PaymentPluginApiException {
         if (makeNextInvoiceFailWithException.getAndSet(false)) {
@@ -92,7 +98,6 @@ public class DefaultNoOpPaymentProviderPlugin implements NoOpPaymentPluginApi {
         return result;
     }
 
-
     @Override
     public PaymentInfoPlugin getPaymentInfo(final UUID paymentId) throws PaymentPluginApiException {
         final PaymentInfoPlugin payment = payments.get(paymentId);
@@ -128,11 +133,9 @@ public class DefaultNoOpPaymentProviderPlugin implements NoOpPaymentPluginApi {
         }
         pms.add(realWithID);
 
-
         return realWithID.getExternalPaymentMethodId();
     }
 
-
     @Override
     public void updatePaymentMethod(final String accountKey, final PaymentMethodPlugin paymentMethodProps)
             throws PaymentPluginApiException {
@@ -144,11 +147,9 @@ public class DefaultNoOpPaymentProviderPlugin implements NoOpPaymentPluginApi {
 
     @Override
     public void deletePaymentMethod(final String accountKey, final String paymentMethodId) throws PaymentPluginApiException {
-
         PaymentMethodPlugin toBeDeleted = null;
         final List<PaymentMethodPlugin> pms = paymentMethods.get(accountKey);
         if (pms != null) {
-
             for (final PaymentMethodPlugin cur : pms) {
                 if (cur.getExternalPaymentMethodId().equals(paymentMethodId)) {
                     toBeDeleted = cur;
@@ -156,6 +157,7 @@ public class DefaultNoOpPaymentProviderPlugin implements NoOpPaymentPluginApi {
                 }
             }
         }
+
         if (toBeDeleted != null) {
             pms.remove(toBeDeleted);
         }
@@ -170,7 +172,42 @@ public class DefaultNoOpPaymentProviderPlugin implements NoOpPaymentPluginApi {
     @Override
     public PaymentMethodPlugin getPaymentMethodDetail(final String accountKey, final String externalPaymentId)
             throws PaymentPluginApiException {
-        return getPaymentMethodDetail(accountKey, externalPaymentId);
+        return getPaymentMethod(accountKey, externalPaymentId);
+    }
+
+    @Override
+    public void setDefaultPaymentMethod(final String accountKey, final String externalPaymentId) throws PaymentPluginApiException {
+    }
+
+    @Override
+    public void processRefund(final Account account, final UUID paymentId, final BigDecimal refundAmount) throws PaymentPluginApiException {
+        final PaymentInfoPlugin paymentInfoPlugin = getPaymentInfo(paymentId);
+        if (paymentInfoPlugin == null) {
+            throw new PaymentPluginApiException("", String.format("No payment found for paymentId %s (plugin %s)", paymentId, getName()));
+        }
+
+        BigDecimal maxAmountRefundable = paymentInfoPlugin.getAmount();
+        for (final BigDecimal refund : refunds.get(paymentId)) {
+            maxAmountRefundable = maxAmountRefundable.add(refund.negate());
+        }
+        if (maxAmountRefundable.compareTo(refundAmount) < 0) {
+            throw new PaymentPluginApiException("", String.format("Refund amount of %s for paymentId %s is bigger than the payment amount %s (plugin %s)",
+                                                                  refundAmount, paymentId, paymentInfoPlugin.getAmount(), getName()));
+        }
+
+        refunds.put(paymentId, refundAmount);
+    }
+
+    @Override
+    public int getNbRefundForPaymentAmount(final Account account, final UUID paymentId, final BigDecimal refundAmount) throws PaymentPluginApiException {
+        int nbRefunds = 0;
+        for (final BigDecimal amount : refunds.get(paymentId)) {
+            if (amount.compareTo(refundAmount) == 0) {
+                nbRefunds++;
+            }
+        }
+
+        return nbRefunds;
     }
 
     private DefaultNoOpPaymentMethodPlugin getPaymentMethod(final String accountKey, final String externalPaymentId) {
@@ -178,28 +215,13 @@ public class DefaultNoOpPaymentProviderPlugin implements NoOpPaymentPluginApi {
         if (pms == null) {
             return null;
         }
+
         for (final PaymentMethodPlugin cur : pms) {
             if (cur.getExternalPaymentMethodId().equals(externalPaymentId)) {
                 return (DefaultNoOpPaymentMethodPlugin) cur;
             }
         }
-        return null;
-    }
-
-    @Override
-    public void setDefaultPaymentMethod(final String accountKey,
-                                        final String externalPaymentId) throws PaymentPluginApiException {
-    }
 
-    @Override
-    public void processRefund(Account account, UUID paymentId,
-            BigDecimal refundAmout) throws PaymentPluginApiException {
-    }
-
-    @Override
-    public int getNbRefundForPaymentAmount(Account account, UUID paymentId,
-            BigDecimal refundAmount) throws PaymentPluginApiException {
-        return 0;
+        return null;
     }
-
 }
diff --git a/payment/src/main/java/com/ning/billing/payment/provider/DefaultPaymentProviderPluginRegistry.java b/payment/src/main/java/com/ning/billing/payment/provider/DefaultPaymentProviderPluginRegistry.java
index 200eaca..2ffa03c 100644
--- a/payment/src/main/java/com/ning/billing/payment/provider/DefaultPaymentProviderPluginRegistry.java
+++ b/payment/src/main/java/com/ning/billing/payment/provider/DefaultPaymentProviderPluginRegistry.java
@@ -20,11 +20,12 @@ import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
-import com.google.common.base.Strings;
-import com.google.inject.Inject;
 import com.ning.billing.config.PaymentConfig;
 import com.ning.billing.payment.plugin.api.PaymentPluginApi;
 
+import com.google.common.base.Strings;
+import com.google.inject.Inject;
+
 public class DefaultPaymentProviderPluginRegistry implements PaymentProviderPluginRegistry {
 
     private final String defaultPlugin;
diff --git a/payment/src/main/java/com/ning/billing/payment/provider/ExternalPaymentProviderPlugin.java b/payment/src/main/java/com/ning/billing/payment/provider/ExternalPaymentProviderPlugin.java
new file mode 100644
index 0000000..52fcb0b
--- /dev/null
+++ b/payment/src/main/java/com/ning/billing/payment/provider/ExternalPaymentProviderPlugin.java
@@ -0,0 +1,43 @@
+/*
+ * 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.payment.provider;
+
+import com.ning.billing.util.clock.Clock;
+
+import com.google.inject.Inject;
+
+/**
+ * Special plugin used to record external payments (i.e. payments not issued by Killbill), such as checks.
+ * <p/>
+ * The implementation is very similar to the no-op plugin, which it extends. This can potentially be an issue
+ * if Killbill is processing a lot of external payments as they are all kept in memory.
+ * TODO: do something about it
+ */
+public class ExternalPaymentProviderPlugin extends DefaultNoOpPaymentProviderPlugin {
+
+    public static final String PLUGIN_NAME = "__EXTERNAL_PAYMENT__";
+
+    @Inject
+    public ExternalPaymentProviderPlugin(final Clock clock) {
+        super(clock);
+    }
+
+    @Override
+    public String getName() {
+        return PLUGIN_NAME;
+    }
+}
diff --git a/payment/src/test/java/com/ning/billing/payment/glue/TestDefaultPaymentProviderPluginRegistryProvider.java b/payment/src/test/java/com/ning/billing/payment/glue/TestDefaultPaymentProviderPluginRegistryProvider.java
new file mode 100644
index 0000000..ef6ffde
--- /dev/null
+++ b/payment/src/test/java/com/ning/billing/payment/glue/TestDefaultPaymentProviderPluginRegistryProvider.java
@@ -0,0 +1,52 @@
+/*
+ * 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.payment.glue;
+
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.config.PaymentConfig;
+import com.ning.billing.payment.PaymentTestSuite;
+import com.ning.billing.payment.provider.ExternalPaymentProviderPlugin;
+import com.ning.billing.payment.provider.PaymentProviderPluginRegistry;
+import com.ning.billing.util.clock.Clock;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+public class TestDefaultPaymentProviderPluginRegistryProvider extends PaymentTestSuite {
+
+    @Test(groups = "fast")
+    public void testInjection() throws Exception {
+        final Injector injector = Guice.createInjector(new AbstractModule() {
+            @Override
+            protected void configure() {
+                bind(PaymentConfig.class).toInstance(Mockito.mock(PaymentConfig.class));
+                bind(Clock.class).toInstance(Mockito.mock(Clock.class));
+
+                bind(PaymentProviderPluginRegistry.class)
+                        .toProvider(DefaultPaymentProviderPluginRegistryProvider.class)
+                        .asEagerSingleton();
+            }
+        });
+
+        final PaymentProviderPluginRegistry registry = injector.getInstance(PaymentProviderPluginRegistry.class);
+        Assert.assertNotNull(registry.getPlugin(ExternalPaymentProviderPlugin.PLUGIN_NAME));
+    }
+}
diff --git a/payment/src/test/java/com/ning/billing/payment/provider/TestDefaultNoOpPaymentInfoPlugin.java b/payment/src/test/java/com/ning/billing/payment/provider/TestDefaultNoOpPaymentInfoPlugin.java
new file mode 100644
index 0000000..158f30e
--- /dev/null
+++ b/payment/src/test/java/com/ning/billing/payment/provider/TestDefaultNoOpPaymentInfoPlugin.java
@@ -0,0 +1,55 @@
+/*
+ * 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.payment.provider;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.joda.time.DateTime;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.payment.PaymentTestSuite;
+import com.ning.billing.payment.plugin.api.PaymentInfoPlugin.PaymentPluginStatus;
+import com.ning.billing.util.clock.Clock;
+import com.ning.billing.util.clock.ClockMock;
+
+public class TestDefaultNoOpPaymentInfoPlugin extends PaymentTestSuite {
+
+    private final Clock clock = new ClockMock();
+
+    @Test(groups = "fast")
+    public void testEquals() throws Exception {
+        final BigDecimal amount = new BigDecimal("1.394810E-3");
+        final DateTime effectiveDate = clock.getUTCNow().plusDays(1);
+        final DateTime createdDate = clock.getUTCNow();
+        final PaymentPluginStatus status = PaymentPluginStatus.UNDEFINED;
+        final String error = UUID.randomUUID().toString();
+
+        final DefaultNoOpPaymentInfoPlugin info = new DefaultNoOpPaymentInfoPlugin(amount, effectiveDate, createdDate,
+                                                                                   status, error);
+        Assert.assertEquals(info, info);
+
+        final DefaultNoOpPaymentInfoPlugin sameInfo = new DefaultNoOpPaymentInfoPlugin(amount, effectiveDate, createdDate,
+                                                                                       status, error);
+        Assert.assertEquals(sameInfo, info);
+
+        final DefaultNoOpPaymentInfoPlugin otherInfo = new DefaultNoOpPaymentInfoPlugin(amount, effectiveDate, createdDate,
+                                                                                        status, UUID.randomUUID().toString());
+        Assert.assertNotEquals(otherInfo, info);
+    }
+}
diff --git a/payment/src/test/java/com/ning/billing/payment/provider/TestDefaultNoOpPaymentMethodPlugin.java b/payment/src/test/java/com/ning/billing/payment/provider/TestDefaultNoOpPaymentMethodPlugin.java
new file mode 100644
index 0000000..d093077
--- /dev/null
+++ b/payment/src/test/java/com/ning/billing/payment/provider/TestDefaultNoOpPaymentMethodPlugin.java
@@ -0,0 +1,63 @@
+/*
+ * 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.payment.provider;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.billing.payment.PaymentTestSuite;
+import com.ning.billing.payment.api.PaymentMethodPlugin.PaymentMethodKVInfo;
+
+import com.google.common.collect.ImmutableList;
+
+public class TestDefaultNoOpPaymentMethodPlugin extends PaymentTestSuite {
+
+    @Test(groups = "fast")
+    public void testEquals() throws Exception {
+        final String externalId = UUID.randomUUID().toString();
+        final boolean isDefault = false;
+        final List<PaymentMethodKVInfo> props = ImmutableList.<PaymentMethodKVInfo>of(new PaymentMethodKVInfo(UUID.randomUUID().toString(), UUID.randomUUID().toString(), false));
+
+        final DefaultNoOpPaymentMethodPlugin paymentMethod = new DefaultNoOpPaymentMethodPlugin(externalId, isDefault, props);
+        Assert.assertEquals(paymentMethod, paymentMethod);
+
+        final DefaultNoOpPaymentMethodPlugin samePaymentMethod = new DefaultNoOpPaymentMethodPlugin(externalId, isDefault, props);
+        Assert.assertEquals(samePaymentMethod, paymentMethod);
+
+        final DefaultNoOpPaymentMethodPlugin otherPaymentMethod = new DefaultNoOpPaymentMethodPlugin(externalId, isDefault, ImmutableList.<PaymentMethodKVInfo>of());
+        Assert.assertNotEquals(otherPaymentMethod, paymentMethod);
+    }
+
+    @Test(groups = "fast")
+    public void testEqualsForPaymentMethodKVInfo() throws Exception {
+        final String key = UUID.randomUUID().toString();
+        final Object value = UUID.randomUUID();
+        final boolean updatable = false;
+
+        final PaymentMethodKVInfo kvInfo = new PaymentMethodKVInfo(key, value, updatable);
+        Assert.assertEquals(kvInfo, kvInfo);
+
+        final PaymentMethodKVInfo sameKvInfo = new PaymentMethodKVInfo(key, value, updatable);
+        Assert.assertEquals(sameKvInfo, kvInfo);
+
+        final PaymentMethodKVInfo otherKvInfo = new PaymentMethodKVInfo(key, value, !updatable);
+        Assert.assertNotEquals(otherKvInfo, kvInfo);
+    }
+}
diff --git a/payment/src/test/java/com/ning/billing/payment/provider/TestExternalPaymentProviderPlugin.java b/payment/src/test/java/com/ning/billing/payment/provider/TestExternalPaymentProviderPlugin.java
new file mode 100644
index 0000000..ff07534
--- /dev/null
+++ b/payment/src/test/java/com/ning/billing/payment/provider/TestExternalPaymentProviderPlugin.java
@@ -0,0 +1,136 @@
+/*
+ * 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.payment.provider;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import org.joda.time.Seconds;
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.ning.billing.account.api.Account;
+import com.ning.billing.payment.PaymentTestSuite;
+import com.ning.billing.payment.plugin.api.PaymentInfoPlugin;
+import com.ning.billing.payment.plugin.api.PaymentInfoPlugin.PaymentPluginStatus;
+import com.ning.billing.payment.plugin.api.PaymentPluginApiException;
+import com.ning.billing.util.clock.Clock;
+import com.ning.billing.util.clock.ClockMock;
+
+public class TestExternalPaymentProviderPlugin extends PaymentTestSuite {
+
+    private final Clock clock = new ClockMock();
+    private ExternalPaymentProviderPlugin plugin;
+
+    @BeforeMethod(groups = "fast")
+    public void setUp() throws Exception {
+        plugin = new ExternalPaymentProviderPlugin(clock);
+    }
+
+    @Test(groups = "fast")
+    public void testGetName() throws Exception {
+        Assert.assertEquals(plugin.getName(), ExternalPaymentProviderPlugin.PLUGIN_NAME);
+    }
+
+    @Test(groups = "fast")
+    public void testProcessPayment() throws Exception {
+        final String externalKey = UUID.randomUUID().toString();
+        final UUID paymentId = UUID.randomUUID();
+        final BigDecimal amount = BigDecimal.TEN;
+        final PaymentInfoPlugin paymentInfoPlugin = plugin.processPayment(externalKey, paymentId, amount);
+
+        Assert.assertEquals(paymentInfoPlugin.getAmount(), amount);
+        Assert.assertEquals(Seconds.secondsBetween(paymentInfoPlugin.getCreatedDate(), clock.getUTCNow()).getSeconds(), 0);
+        Assert.assertEquals(Seconds.secondsBetween(paymentInfoPlugin.getEffectiveDate(), clock.getUTCNow()).getSeconds(), 0);
+        Assert.assertNull(paymentInfoPlugin.getExtFirstReferenceId());
+        Assert.assertNull(paymentInfoPlugin.getExtSecondReferenceId());
+        Assert.assertNull(paymentInfoPlugin.getGatewayError());
+        Assert.assertNull(paymentInfoPlugin.getGatewayErrorCode());
+        Assert.assertEquals(paymentInfoPlugin.getStatus(), PaymentPluginStatus.PROCESSED);
+
+        final PaymentInfoPlugin retrievedPaymentInfoPlugin = plugin.getPaymentInfo(paymentId);
+        Assert.assertEquals(retrievedPaymentInfoPlugin, paymentInfoPlugin);
+    }
+
+    @Test(groups = "fast", expectedExceptions = PaymentPluginApiException.class)
+    public void testRefundForNonExistingPayment() throws Exception {
+        plugin.processRefund(Mockito.mock(Account.class), UUID.randomUUID(), BigDecimal.ONE);
+    }
+
+    @Test(groups = "fast", expectedExceptions = PaymentPluginApiException.class)
+    public void testRefundTooLarge() throws Exception {
+        final UUID paymentId = UUID.randomUUID();
+        plugin.processPayment(UUID.randomUUID().toString(), paymentId, BigDecimal.ZERO);
+
+        plugin.processRefund(Mockito.mock(Account.class), paymentId, BigDecimal.ONE);
+    }
+
+    @Test(groups = "fast")
+    public void testRefundTooLargeMultipleTimes() throws Exception {
+        final UUID paymentId = UUID.randomUUID();
+        plugin.processPayment(UUID.randomUUID().toString(), paymentId, BigDecimal.TEN);
+
+        final Account account = Mockito.mock(Account.class);
+        for (int i = 0; i < 10; i++) {
+            plugin.processRefund(account, paymentId, BigDecimal.ONE);
+        }
+
+        try {
+            plugin.processRefund(account, paymentId, BigDecimal.ONE);
+            Assert.fail("Shouldn't have been able to refund");
+        } catch (PaymentPluginApiException e) {
+            Assert.assertTrue(true);
+        }
+    }
+
+    @Test(groups = "fast")
+    public void testRefund() throws Exception {
+        // An external payment refund would be e.g. a check that we trash
+        final String externalKey = UUID.randomUUID().toString();
+        final UUID paymentId = UUID.randomUUID();
+        final BigDecimal amount = BigDecimal.TEN;
+        plugin.processPayment(externalKey, paymentId, amount);
+
+        plugin.processRefund(Mockito.mock(Account.class), paymentId, BigDecimal.ONE);
+        Assert.assertEquals(plugin.getNbRefundForPaymentAmount(Mockito.mock(Account.class), UUID.randomUUID(), BigDecimal.ONE), 0);
+        Assert.assertEquals(plugin.getNbRefundForPaymentAmount(Mockito.mock(Account.class), paymentId, BigDecimal.TEN), 0);
+        Assert.assertEquals(plugin.getNbRefundForPaymentAmount(Mockito.mock(Account.class), paymentId, BigDecimal.ONE), 1);
+        Assert.assertEquals(plugin.getNbRefundForPaymentAmount(Mockito.mock(Account.class), paymentId, new BigDecimal("5")), 0);
+
+        // Try multiple refunds
+
+        plugin.processRefund(Mockito.mock(Account.class), paymentId, BigDecimal.ONE);
+        Assert.assertEquals(plugin.getNbRefundForPaymentAmount(Mockito.mock(Account.class), UUID.randomUUID(), BigDecimal.ONE), 0);
+        Assert.assertEquals(plugin.getNbRefundForPaymentAmount(Mockito.mock(Account.class), paymentId, BigDecimal.TEN), 0);
+        Assert.assertEquals(plugin.getNbRefundForPaymentAmount(Mockito.mock(Account.class), paymentId, BigDecimal.ONE), 2);
+        Assert.assertEquals(plugin.getNbRefundForPaymentAmount(Mockito.mock(Account.class), paymentId, new BigDecimal("5")), 0);
+
+        plugin.processRefund(Mockito.mock(Account.class), paymentId, BigDecimal.ONE);
+        Assert.assertEquals(plugin.getNbRefundForPaymentAmount(Mockito.mock(Account.class), UUID.randomUUID(), BigDecimal.ONE), 0);
+        Assert.assertEquals(plugin.getNbRefundForPaymentAmount(Mockito.mock(Account.class), paymentId, BigDecimal.TEN), 0);
+        Assert.assertEquals(plugin.getNbRefundForPaymentAmount(Mockito.mock(Account.class), paymentId, BigDecimal.ONE), 3);
+        Assert.assertEquals(plugin.getNbRefundForPaymentAmount(Mockito.mock(Account.class), paymentId, new BigDecimal("5")), 0);
+
+        plugin.processRefund(Mockito.mock(Account.class), paymentId, new BigDecimal("5"));
+        Assert.assertEquals(plugin.getNbRefundForPaymentAmount(Mockito.mock(Account.class), UUID.randomUUID(), BigDecimal.ONE), 0);
+        Assert.assertEquals(plugin.getNbRefundForPaymentAmount(Mockito.mock(Account.class), paymentId, BigDecimal.TEN), 0);
+        Assert.assertEquals(plugin.getNbRefundForPaymentAmount(Mockito.mock(Account.class), paymentId, BigDecimal.ONE), 3);
+        Assert.assertEquals(plugin.getNbRefundForPaymentAmount(Mockito.mock(Account.class), paymentId, new BigDecimal("5")), 1);
+    }
+}