killbill-uncached

payment: implement hosted pages APIs Signed-off-by: Pierre-Alexandre

4/28/2014 4:29:33 PM

Changes

pom.xml 2(+1 -1)

Details

diff --git a/beatrix/src/test/resources/killbill-currency-plugin-test.tar.gz b/beatrix/src/test/resources/killbill-currency-plugin-test.tar.gz
index 0006981..cec5cea 100644
Binary files a/beatrix/src/test/resources/killbill-currency-plugin-test.tar.gz and b/beatrix/src/test/resources/killbill-currency-plugin-test.tar.gz differ
diff --git a/beatrix/src/test/resources/killbill-notification-test.tar.gz b/beatrix/src/test/resources/killbill-notification-test.tar.gz
index b03cd00..41a496e 100644
Binary files a/beatrix/src/test/resources/killbill-notification-test.tar.gz and b/beatrix/src/test/resources/killbill-notification-test.tar.gz differ
diff --git a/beatrix/src/test/resources/killbill-payment-test.tar.gz b/beatrix/src/test/resources/killbill-payment-test.tar.gz
index 7263203..2bf919b 100644
Binary files a/beatrix/src/test/resources/killbill-payment-test.tar.gz and b/beatrix/src/test/resources/killbill-payment-test.tar.gz differ
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/GatewayNotificationJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/GatewayNotificationJson.java
new file mode 100644
index 0000000..04dfd7c
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/GatewayNotificationJson.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2014 Groupon, Inc
+ * Copyright 2014 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.jaxrs.json;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.ResponseBuilder;
+import javax.ws.rs.core.Response.Status;
+
+import org.killbill.billing.payment.plugin.api.GatewayNotification;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class GatewayNotificationJson extends JsonBase {
+
+    private final String kbPaymentId;
+    private final Integer status;
+    private final String entity;
+    private final Map<String, List<String>> headers;
+    private final Map<String, String> properties;
+
+    @JsonCreator
+    public GatewayNotificationJson(@JsonProperty("kbPaymentId") final String kbPaymentId,
+                                   @JsonProperty("status") final Integer status,
+                                   @JsonProperty("entity") final String entity,
+                                   @JsonProperty("headers") final Map<String, List<String>> headers,
+                                   @JsonProperty("properties") final Map<String, String> properties) {
+        this.kbPaymentId = kbPaymentId;
+        this.status = status;
+        this.entity = entity;
+        this.headers = headers;
+        this.properties = properties;
+    }
+
+    public GatewayNotificationJson(final GatewayNotification notification) {
+        this.kbPaymentId = notification.getKbPaymentId().toString();
+        this.status = notification.getStatus();
+        this.entity = notification.getEntity();
+        this.headers = notification.getHeaders();
+        this.properties = propertiesToMap(notification.getProperties());
+    }
+
+    public Response toResponse() {
+        final ResponseBuilder responseBuilder = Response.status(status == null ? Status.OK : Status.fromStatusCode(status));
+        if (entity != null) {
+            responseBuilder.entity(entity);
+        }
+        if (headers != null) {
+            for (final String key : headers.keySet()) {
+                if (headers.get(key) != null) {
+                    for (final String value : headers.get(key)) {
+                        responseBuilder.header(key, value);
+                    }
+                }
+            }
+        }
+
+        return responseBuilder.build();
+    }
+
+    public String getKbPaymentId() {
+        return kbPaymentId;
+    }
+
+    public Integer getStatus() {
+        return status;
+    }
+
+    public String getEntity() {
+        return entity;
+    }
+
+    public Map<String, List<String>> getHeaders() {
+        return headers;
+    }
+
+    public Map<String, String> getProperties() {
+        return properties;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuffer sb = new StringBuffer("GatewayNotificationJson{");
+        sb.append("kbPaymentId='").append(kbPaymentId).append('\'');
+        sb.append(", status=").append(status);
+        sb.append(", entity='").append(entity).append('\'');
+        sb.append(", headers=").append(headers);
+        sb.append(", properties=").append(properties);
+        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 GatewayNotificationJson that = (GatewayNotificationJson) o;
+
+        if (entity != null ? !entity.equals(that.entity) : that.entity != null) {
+            return false;
+        }
+        if (headers != null ? !headers.equals(that.headers) : that.headers != null) {
+            return false;
+        }
+        if (kbPaymentId != null ? !kbPaymentId.equals(that.kbPaymentId) : that.kbPaymentId != null) {
+            return false;
+        }
+        if (properties != null ? !properties.equals(that.properties) : that.properties != null) {
+            return false;
+        }
+        if (status != null ? !status.equals(that.status) : that.status != null) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = kbPaymentId != null ? kbPaymentId.hashCode() : 0;
+        result = 31 * result + (status != null ? status.hashCode() : 0);
+        result = 31 * result + (entity != null ? entity.hashCode() : 0);
+        result = 31 * result + (headers != null ? headers.hashCode() : 0);
+        result = 31 * result + (properties != null ? properties.hashCode() : 0);
+        return result;
+    }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/HostedPaymentPageFieldsJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/HostedPaymentPageFieldsJson.java
index 2355bbe..470f8f8 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/HostedPaymentPageFieldsJson.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/HostedPaymentPageFieldsJson.java
@@ -16,183 +16,28 @@
 
 package org.killbill.billing.jaxrs.json;
 
-import java.math.BigDecimal;
-import java.util.Map;
-
-import org.killbill.billing.catalog.api.Currency;
+import java.util.List;
 
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
 
 public class HostedPaymentPageFieldsJson extends JsonBase {
 
-    private final String credential2;
-    private final String credential3;
-    private final String credential4;
-    private final BigDecimal amount;
-    private final Currency currency;
-    private final String transactionType;
-    private final String authCode;
-    private final String notifyUrl;
-    private final String returnUrl;
-    private final String forwardUrl;
-    private final String cancelReturnUrl;
-    private final String redirectParam;
-    private final String accountName;
-    private final HostedPaymentPageCustomerJson customer;
-    private final HostedPaymentPageBillingAddressJson billingAddress;
-    private final String order;
-    private final String description;
-    private final String tax;
-    private final String shipping;
-    private final Map<String, String> customFields;
+    private final List<PluginPropertyJson> formFields;
 
     @JsonCreator
-    public HostedPaymentPageFieldsJson(@JsonProperty("credential2") final String credential2,
-                                       @JsonProperty("credential3") final String credential3,
-                                       @JsonProperty("credential4") final String credential4,
-                                       @JsonProperty("amount") final BigDecimal amount,
-                                       @JsonProperty("currency") final Currency currency,
-                                       @JsonProperty("transactionType") final String transactionType,
-                                       @JsonProperty("authCode") final String authCode,
-                                       @JsonProperty("notifyUrl") final String notifyUrl,
-                                       @JsonProperty("returnUrl") final String returnUrl,
-                                       @JsonProperty("forwardUrl") final String forwardUrl,
-                                       @JsonProperty("cancelReturnUrl") final String cancelReturnUrl,
-                                       @JsonProperty("redirectParam") final String redirectParam,
-                                       @JsonProperty("accountName") final String accountName,
-                                       @JsonProperty("customer") final HostedPaymentPageCustomerJson customer,
-                                       @JsonProperty("billingAddress") final HostedPaymentPageBillingAddressJson billingAddress,
-                                       @JsonProperty("order") final String order,
-                                       @JsonProperty("description") final String description,
-                                       @JsonProperty("tax") final String tax,
-                                       @JsonProperty("shipping") final String shipping,
-                                       @JsonProperty("customFields") final Map<String, String> customFields) {
-        this.credential2 = credential2;
-        this.credential3 = credential3;
-        this.credential4 = credential4;
-        this.amount = amount;
-        this.currency = currency;
-        this.transactionType = transactionType;
-        this.authCode = authCode;
-        this.notifyUrl = notifyUrl;
-        this.returnUrl = returnUrl;
-        this.forwardUrl = forwardUrl;
-        this.cancelReturnUrl = cancelReturnUrl;
-        this.redirectParam = redirectParam;
-        this.accountName = accountName;
-        this.customer = customer;
-        this.billingAddress = billingAddress;
-        this.order = order;
-        this.description = description;
-        this.tax = tax;
-        this.shipping = shipping;
-        this.customFields = customFields;
-    }
-
-    public String getCredential2() {
-        return credential2;
-    }
-
-    public String getCredential3() {
-        return credential3;
-    }
-
-    public String getCredential4() {
-        return credential4;
-    }
-
-    public BigDecimal getAmount() {
-        return amount;
-    }
-
-    public Currency getCurrency() {
-        return currency;
-    }
-
-    public String getTransactionType() {
-        return transactionType;
-    }
-
-    public String getAuthCode() {
-        return authCode;
-    }
-
-    public String getNotifyUrl() {
-        return notifyUrl;
-    }
-
-    public String getReturnUrl() {
-        return returnUrl;
-    }
-
-    public String getForwardUrl() {
-        return forwardUrl;
-    }
-
-    public String getCancelReturnUrl() {
-        return cancelReturnUrl;
-    }
-
-    public String getRedirectParam() {
-        return redirectParam;
-    }
-
-    public String getAccountName() {
-        return accountName;
-    }
-
-    public HostedPaymentPageCustomerJson getCustomer() {
-        return customer;
-    }
-
-    public HostedPaymentPageBillingAddressJson getBillingAddress() {
-        return billingAddress;
-    }
-
-    public String getOrder() {
-        return order;
-    }
-
-    public String getDescription() {
-        return description;
-    }
-
-    public String getTax() {
-        return tax;
-    }
-
-    public String getShipping() {
-        return shipping;
+    public HostedPaymentPageFieldsJson(@JsonProperty("formFields") final List<PluginPropertyJson> formFields) {
+        this.formFields = formFields;
     }
 
-    public Map<String, String> getCustomFields() {
-        return customFields;
+    public List<PluginPropertyJson> getCustomFields() {
+        return formFields;
     }
 
     @Override
     public String toString() {
         final StringBuffer sb = new StringBuffer("HostedPaymentPageFieldsJson{");
-        sb.append("credential2='").append(credential2).append('\'');
-        sb.append(", credential3='").append(credential3).append('\'');
-        sb.append(", credential4='").append(credential4).append('\'');
-        sb.append(", amount=").append(amount);
-        sb.append(", currency=").append(currency);
-        sb.append(", transactionType='").append(transactionType).append('\'');
-        sb.append(", authCode='").append(authCode).append('\'');
-        sb.append(", notifyUrl='").append(notifyUrl).append('\'');
-        sb.append(", returnUrl='").append(returnUrl).append('\'');
-        sb.append(", forwardUrl='").append(forwardUrl).append('\'');
-        sb.append(", cancelReturnUrl='").append(cancelReturnUrl).append('\'');
-        sb.append(", redirectParam='").append(redirectParam).append('\'');
-        sb.append(", accountName='").append(accountName).append('\'');
-        sb.append(", customer=").append(customer);
-        sb.append(", billingAddress=").append(billingAddress);
-        sb.append(", order='").append(order).append('\'');
-        sb.append(", description='").append(description).append('\'');
-        sb.append(", tax='").append(tax).append('\'');
-        sb.append(", shipping='").append(shipping).append('\'');
-        sb.append(", customFields=").append(customFields);
+        sb.append(", formFields=").append(formFields);
         sb.append('}');
         return sb.toString();
     }
@@ -208,64 +53,7 @@ public class HostedPaymentPageFieldsJson extends JsonBase {
 
         final HostedPaymentPageFieldsJson that = (HostedPaymentPageFieldsJson) o;
 
-        if (accountName != null ? !accountName.equals(that.accountName) : that.accountName != null) {
-            return false;
-        }
-        if (amount != null ? !amount.equals(that.amount) : that.amount != null) {
-            return false;
-        }
-        if (authCode != null ? !authCode.equals(that.authCode) : that.authCode != null) {
-            return false;
-        }
-        if (billingAddress != null ? !billingAddress.equals(that.billingAddress) : that.billingAddress != null) {
-            return false;
-        }
-        if (cancelReturnUrl != null ? !cancelReturnUrl.equals(that.cancelReturnUrl) : that.cancelReturnUrl != null) {
-            return false;
-        }
-        if (credential2 != null ? !credential2.equals(that.credential2) : that.credential2 != null) {
-            return false;
-        }
-        if (credential3 != null ? !credential3.equals(that.credential3) : that.credential3 != null) {
-            return false;
-        }
-        if (credential4 != null ? !credential4.equals(that.credential4) : that.credential4 != null) {
-            return false;
-        }
-        if (currency != that.currency) {
-            return false;
-        }
-        if (customFields != null ? !customFields.equals(that.customFields) : that.customFields != null) {
-            return false;
-        }
-        if (customer != null ? !customer.equals(that.customer) : that.customer != null) {
-            return false;
-        }
-        if (description != null ? !description.equals(that.description) : that.description != null) {
-            return false;
-        }
-        if (forwardUrl != null ? !forwardUrl.equals(that.forwardUrl) : that.forwardUrl != null) {
-            return false;
-        }
-        if (notifyUrl != null ? !notifyUrl.equals(that.notifyUrl) : that.notifyUrl != null) {
-            return false;
-        }
-        if (order != null ? !order.equals(that.order) : that.order != null) {
-            return false;
-        }
-        if (redirectParam != null ? !redirectParam.equals(that.redirectParam) : that.redirectParam != null) {
-            return false;
-        }
-        if (returnUrl != null ? !returnUrl.equals(that.returnUrl) : that.returnUrl != null) {
-            return false;
-        }
-        if (shipping != null ? !shipping.equals(that.shipping) : that.shipping != null) {
-            return false;
-        }
-        if (tax != null ? !tax.equals(that.tax) : that.tax != null) {
-            return false;
-        }
-        if (transactionType != null ? !transactionType.equals(that.transactionType) : that.transactionType != null) {
+        if (formFields != null ? !formFields.equals(that.formFields) : that.formFields != null) {
             return false;
         }
 
@@ -274,26 +62,6 @@ public class HostedPaymentPageFieldsJson extends JsonBase {
 
     @Override
     public int hashCode() {
-        int result = credential2 != null ? credential2.hashCode() : 0;
-        result = 31 * result + (credential3 != null ? credential3.hashCode() : 0);
-        result = 31 * result + (credential4 != null ? credential4.hashCode() : 0);
-        result = 31 * result + (amount != null ? amount.hashCode() : 0);
-        result = 31 * result + (currency != null ? currency.hashCode() : 0);
-        result = 31 * result + (transactionType != null ? transactionType.hashCode() : 0);
-        result = 31 * result + (authCode != null ? authCode.hashCode() : 0);
-        result = 31 * result + (notifyUrl != null ? notifyUrl.hashCode() : 0);
-        result = 31 * result + (returnUrl != null ? returnUrl.hashCode() : 0);
-        result = 31 * result + (forwardUrl != null ? forwardUrl.hashCode() : 0);
-        result = 31 * result + (cancelReturnUrl != null ? cancelReturnUrl.hashCode() : 0);
-        result = 31 * result + (redirectParam != null ? redirectParam.hashCode() : 0);
-        result = 31 * result + (accountName != null ? accountName.hashCode() : 0);
-        result = 31 * result + (customer != null ? customer.hashCode() : 0);
-        result = 31 * result + (billingAddress != null ? billingAddress.hashCode() : 0);
-        result = 31 * result + (order != null ? order.hashCode() : 0);
-        result = 31 * result + (description != null ? description.hashCode() : 0);
-        result = 31 * result + (tax != null ? tax.hashCode() : 0);
-        result = 31 * result + (shipping != null ? shipping.hashCode() : 0);
-        result = 31 * result + (customFields != null ? customFields.hashCode() : 0);
-        return result;
+        return formFields != null ? formFields.hashCode() : 0;
     }
 }
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/HostedPaymentPageFormDescriptorJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/HostedPaymentPageFormDescriptorJson.java
new file mode 100644
index 0000000..2753e70
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/HostedPaymentPageFormDescriptorJson.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2014 Groupon, Inc
+ * Copyright 2014 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.jaxrs.json;
+
+import java.util.Map;
+
+import org.killbill.billing.payment.plugin.api.HostedPaymentPageFormDescriptor;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class HostedPaymentPageFormDescriptorJson extends JsonBase {
+
+    private final String kbAccountId;
+    private final String formMethod;
+    private final String formUrl;
+    private final Map<String, String> formFields;
+    private final Map<String, String> properties;
+
+    @JsonCreator
+    public HostedPaymentPageFormDescriptorJson(@JsonProperty("kbAccountId") final String kbAccountId,
+                                               @JsonProperty("formMethod") final String formMethod,
+                                               @JsonProperty("formUrl") final String formUrl,
+                                               @JsonProperty("formFields") final Map<String, String> formFields,
+                                               @JsonProperty("properties") final Map<String, String> properties) {
+        this.kbAccountId = kbAccountId;
+        this.formMethod = formMethod;
+        this.formUrl = formUrl;
+        this.formFields = formFields;
+        this.properties = properties;
+    }
+
+    public HostedPaymentPageFormDescriptorJson(final HostedPaymentPageFormDescriptor descriptor) {
+        this.kbAccountId = descriptor.getKbAccountId().toString();
+        this.formMethod = descriptor.getFormMethod();
+        this.formUrl = descriptor.getFormUrl();
+        this.formFields = propertiesToMap(descriptor.getFormFields());
+        this.properties = propertiesToMap(descriptor.getProperties());
+    }
+
+    public String getKbAccountId() {
+        return kbAccountId;
+    }
+
+    public String getFormMethod() {
+        return formMethod;
+    }
+
+    public String getFormUrl() {
+        return formUrl;
+    }
+
+    public Map<String, String> getFormFields() {
+        return formFields;
+    }
+
+    public Map<String, String> getProperties() {
+        return properties;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuffer sb = new StringBuffer("HostedPaymentPageFormDescriptorJson{");
+        sb.append("kbAccountId='").append(kbAccountId).append('\'');
+        sb.append(", formMethod='").append(formMethod).append('\'');
+        sb.append(", formUrl='").append(formUrl).append('\'');
+        sb.append(", formFields=").append(formFields);
+        sb.append(", properties=").append(properties);
+        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 HostedPaymentPageFormDescriptorJson that = (HostedPaymentPageFormDescriptorJson) o;
+
+        if (formFields != null ? !formFields.equals(that.formFields) : that.formFields != null) {
+            return false;
+        }
+        if (formMethod != null ? !formMethod.equals(that.formMethod) : that.formMethod != null) {
+            return false;
+        }
+        if (formUrl != null ? !formUrl.equals(that.formUrl) : that.formUrl != null) {
+            return false;
+        }
+        if (kbAccountId != null ? !kbAccountId.equals(that.kbAccountId) : that.kbAccountId != null) {
+            return false;
+        }
+        if (properties != null ? !properties.equals(that.properties) : that.properties != null) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = kbAccountId != null ? kbAccountId.hashCode() : 0;
+        result = 31 * result + (formMethod != null ? formMethod.hashCode() : 0);
+        result = 31 * result + (formUrl != null ? formUrl.hashCode() : 0);
+        result = 31 * result + (formFields != null ? formFields.hashCode() : 0);
+        result = 31 * result + (properties != null ? properties.hashCode() : 0);
+        return result;
+    }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/JsonBase.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/JsonBase.java
index 9ac1001..f73272d 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/JsonBase.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/JsonBase.java
@@ -16,11 +16,15 @@
 
 package org.killbill.billing.jaxrs.json;
 
+import java.util.HashMap;
+import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 import java.util.UUID;
 
 import javax.annotation.Nullable;
 
+import org.killbill.billing.payment.api.PluginProperty;
 import org.killbill.billing.util.audit.AuditLog;
 
 import com.google.common.base.Function;
@@ -59,4 +63,23 @@ public abstract class JsonBase {
     public List<AuditLogJson> getAuditLogs() {
         return auditLogs;
     }
+
+    protected List<PluginProperty> propertiesToList(final Map<String, String> propertiesMap) {
+        final List<PluginProperty> properties = new LinkedList<PluginProperty>();
+        for (final String key : propertiesMap.keySet()) {
+            final PluginProperty property = new PluginProperty(key, propertiesMap.get(key), false);
+            properties.add(property);
+        }
+        return properties;
+    }
+
+    protected Map<String, String> propertiesToMap(final Iterable<PluginProperty> properties) {
+        final Map<String, String> propertiesMap = new HashMap<String, String>();
+        for (final PluginProperty pluginProperty : properties) {
+            if (pluginProperty.getValue() != null) {
+                propertiesMap.put(pluginProperty.getKey(), pluginProperty.getValue().toString());
+            }
+        }
+        return propertiesMap;
+    }
 }
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PaymentMethodJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PaymentMethodJson.java
index f78fe38..9acf598 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PaymentMethodJson.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PaymentMethodJson.java
@@ -65,12 +65,12 @@ public class PaymentMethodJson extends JsonBase {
         final PaymentMethodPlugin pluginDetail = in.getPluginDetail();
         PaymentMethodPluginDetailJson pluginDetailJson = null;
         if (pluginDetail != null) {
-            List<PaymentMethodProperties> properties = null;
+            List<PluginPropertyJson> properties = null;
             if (pluginDetail.getProperties() != null) {
-                properties = new ArrayList<PaymentMethodJson.PaymentMethodProperties>(Collections2.transform(pluginDetail.getProperties(), new Function<PluginProperty, PaymentMethodProperties>() {
+                properties = new ArrayList<PluginPropertyJson>(Collections2.transform(pluginDetail.getProperties(), new Function<PluginProperty, PluginPropertyJson>() {
                     @Override
-                    public PaymentMethodProperties apply(final PluginProperty input) {
-                        return new PaymentMethodProperties(input.getKey(), input.getValue() == null ? null : input.getValue().toString(), input.getIsUpdatable());
+                    public PluginPropertyJson apply(final PluginProperty input) {
+                        return new PluginPropertyJson(input.getKey(), input.getValue() == null ? null : input.getValue().toString(), input.getIsUpdatable());
                     }
                 }));
             }
@@ -221,8 +221,8 @@ public class PaymentMethodJson extends JsonBase {
                     public List<PluginProperty> getProperties() {
                         if (pluginInfo.getProperties() != null) {
                             final List<PluginProperty> result = new LinkedList<PluginProperty>();
-                            for (final PaymentMethodProperties cur : pluginInfo.getProperties()) {
-                                result.add(new PluginProperty(cur.getKey(), cur.getValue(), cur.isUpdatable));
+                            for (final PluginPropertyJson cur : pluginInfo.getProperties()) {
+                                result.add(new PluginProperty(cur.getKey(), cur.getValue(), cur.getIsUpdatable()));
                             }
                             return result;
                         }
@@ -322,7 +322,7 @@ public class PaymentMethodJson extends JsonBase {
         private final String state;
         private final String zip;
         private final String country;
-        private final List<PaymentMethodProperties> properties;
+        private final List<PluginPropertyJson> properties;
 
         @JsonCreator
         public PaymentMethodPluginDetailJson(@JsonProperty("externalPaymentId") final String externalPaymentId,
@@ -339,7 +339,7 @@ public class PaymentMethodJson extends JsonBase {
                                              @JsonProperty("state") final String state,
                                              @JsonProperty("zip") final String zip,
                                              @JsonProperty("country") final String country,
-                                             @JsonProperty("properties") final List<PaymentMethodProperties> properties) {
+                                             @JsonProperty("properties") final List<PluginPropertyJson> properties) {
             this.externalPaymentId = externalPaymentId;
             this.isDefaultPaymentMethod = isDefaultPaymentMethod;
             this.type = type;
@@ -413,7 +413,7 @@ public class PaymentMethodJson extends JsonBase {
             return country;
         }
 
-        public List<PaymentMethodProperties> getProperties() {
+        public List<PluginPropertyJson> getProperties() {
             return properties;
         }
 
@@ -519,75 +519,4 @@ public class PaymentMethodJson extends JsonBase {
             return result;
         }
     }
-
-    public static final class PaymentMethodProperties {
-
-        private final String key;
-        private final String value;
-        private final Boolean isUpdatable;
-
-        @JsonCreator
-        public PaymentMethodProperties(@JsonProperty("key") final String key,
-                                       @JsonProperty("value") final String value,
-                                       @JsonProperty("isUpdatable") final Boolean isUpdatable) {
-            super();
-            this.key = key;
-            this.value = value;
-            this.isUpdatable = isUpdatable;
-        }
-
-        public String getKey() {
-            return key;
-        }
-
-        public String getValue() {
-            return value;
-        }
-
-        public Boolean getIsUpdatable() {
-            return isUpdatable;
-        }
-
-        @Override
-        public String toString() {
-            final StringBuilder sb = new StringBuilder("PaymentMethodProperties{");
-            sb.append("key='").append(key).append('\'');
-            sb.append(", value='").append(value).append('\'');
-            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 PaymentMethodProperties that = (PaymentMethodProperties) 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/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PluginPropertyJson.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PluginPropertyJson.java
new file mode 100644
index 0000000..40c02c5
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/json/PluginPropertyJson.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2014 Groupon, Inc
+ * Copyright 2014 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.jaxrs.json;
+
+import org.killbill.billing.payment.api.PluginProperty;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class PluginPropertyJson {
+
+    private final String key;
+    private final String value;
+    private final Boolean isUpdatable;
+
+    @JsonCreator
+    public PluginPropertyJson(@JsonProperty("key") final String key,
+                              @JsonProperty("value") final String value,
+                              @JsonProperty("isUpdatable") final Boolean isUpdatable) {
+        this.key = key;
+        this.value = value;
+        this.isUpdatable = isUpdatable;
+    }
+
+    public PluginPropertyJson(final PluginProperty pluginProperty) {
+        this(pluginProperty.getKey(), pluginProperty.getValue() == null ? null : pluginProperty.getValue().toString(), pluginProperty.getIsUpdatable());
+    }
+
+    public PluginProperty toPluginProperty() {
+        return new PluginProperty(key, value, isUpdatable);
+    }
+
+    public String getKey() {
+        return key;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    public Boolean getIsUpdatable() {
+        return isUpdatable;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder("PluginPropertyJson{");
+        sb.append("key='").append(key).append('\'');
+        sb.append(", value='").append(value).append('\'');
+        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 PluginPropertyJson that = (PluginPropertyJson) 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/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java
index 0b9df0f..3360b9d 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java
@@ -145,6 +145,9 @@ public interface JaxrsResource {
     public static final String DIRECT_PAYMENTS = "directPayments";
     public static final String DIRECT_PAYMENTS_PATH = PREFIX + "/" + DIRECT_PAYMENTS;
 
+    public static final String PAYMENT_GATEWAYS = "paymentGateways";
+    public static final String PAYMENT_GATEWAYS_PATH = PREFIX + "/" + PAYMENT_GATEWAYS;
+
     public static final String REFUNDS = "refunds";
     public static final String REFUNDS_PATH = PREFIX + "/" + "refunds";
 
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentGatewayResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentGatewayResource.java
new file mode 100644
index 0000000..e7c6aea
--- /dev/null
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentGatewayResource.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2014 Groupon, Inc
+ * Copyright 2014 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.jaxrs.resources;
+
+import java.util.List;
+import java.util.UUID;
+
+import javax.inject.Inject;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.HeaderParam;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.account.api.AccountApiException;
+import org.killbill.billing.account.api.AccountUserApi;
+import org.killbill.billing.jaxrs.json.GatewayNotificationJson;
+import org.killbill.billing.jaxrs.json.HostedPaymentPageFieldsJson;
+import org.killbill.billing.jaxrs.json.HostedPaymentPageFormDescriptorJson;
+import org.killbill.billing.jaxrs.json.PluginPropertyJson;
+import org.killbill.billing.jaxrs.util.Context;
+import org.killbill.billing.jaxrs.util.JaxrsUriBuilder;
+import org.killbill.billing.payment.api.PaymentApiException;
+import org.killbill.billing.payment.api.PaymentGatewayApi;
+import org.killbill.billing.payment.api.PluginProperty;
+import org.killbill.billing.payment.plugin.api.GatewayNotification;
+import org.killbill.billing.payment.plugin.api.HostedPaymentPageFormDescriptor;
+import org.killbill.billing.util.api.AuditUserApi;
+import org.killbill.billing.util.api.CustomFieldUserApi;
+import org.killbill.billing.util.api.TagUserApi;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.clock.Clock;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Iterables;
+
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
+import static javax.ws.rs.core.MediaType.WILDCARD;
+
+@Path(JaxrsResource.PAYMENT_GATEWAYS_PATH)
+public class PaymentGatewayResource extends JaxRsResourceBase {
+
+    private final PaymentGatewayApi paymentGatewayApi;
+
+    @Inject
+    public PaymentGatewayResource(final JaxrsUriBuilder uriBuilder,
+                                  final TagUserApi tagUserApi,
+                                  final CustomFieldUserApi customFieldUserApi,
+                                  final AuditUserApi auditUserApi,
+                                  final AccountUserApi accountUserApi,
+                                  final PaymentGatewayApi paymentGatewayApi,
+                                  final Clock clock,
+                                  final Context context) {
+        super(uriBuilder, tagUserApi, customFieldUserApi, auditUserApi, accountUserApi, clock, context);
+        this.paymentGatewayApi = paymentGatewayApi;
+    }
+
+    @POST
+    @Path("/" + HOSTED + "/" + FORM + "/{" + QUERY_PAYMENT_PLUGIN_NAME + ":" + ANYTHING_PATTERN + "}/" + "/{" + QUERY_ACCOUNT_ID + ":" + UUID_PATTERN + "}")
+    @Consumes(APPLICATION_JSON)
+    @Produces(APPLICATION_JSON)
+    // Generate form data to redirect the customer to the gateway
+    public Response buildFormDescriptor(final HostedPaymentPageFieldsJson json,
+                                        @PathParam(QUERY_ACCOUNT_ID) final String accountIdString,
+                                        @PathParam(QUERY_PAYMENT_PLUGIN_NAME) final String pluginName,
+                                        @QueryParam(QUERY_PLUGIN_PROPERTY) final List<String> pluginPropertiesString,
+                                        @HeaderParam(HDR_CREATED_BY) final String createdBy,
+                                        @HeaderParam(HDR_REASON) final String reason,
+                                        @HeaderParam(HDR_COMMENT) final String comment,
+                                        @javax.ws.rs.core.Context final UriInfo uriInfo,
+                                        @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException, AccountApiException {
+        final Iterable<PluginProperty> pluginProperties = extractPluginProperties(pluginPropertiesString);
+        final CallContext callContext = context.createContext(createdBy, reason, comment, request);
+        final UUID accountId = UUID.fromString(accountIdString);
+        final Account account = accountUserApi.getAccountById(accountId, callContext);
+
+        final Iterable<PluginProperty> customFields = Iterables.<PluginPropertyJson, PluginProperty>transform(json.getCustomFields(),
+                                                                                                              new Function<PluginPropertyJson, PluginProperty>() {
+                                                                                                                  @Override
+                                                                                                                  public PluginProperty apply(final PluginPropertyJson pluginPropertyJson) {
+                                                                                                                      return pluginPropertyJson.toPluginProperty();
+                                                                                                                  }
+                                                                                                              }
+                                                                                                             );
+        final HostedPaymentPageFormDescriptor descriptor = paymentGatewayApi.buildFormDescriptor(account, customFields, pluginProperties, callContext);
+        final HostedPaymentPageFormDescriptorJson result = new HostedPaymentPageFormDescriptorJson(descriptor);
+
+        return Response.status(Response.Status.OK).entity(result).build();
+    }
+
+    @POST
+    @Path("/{" + QUERY_PAYMENT_PLUGIN_NAME + ":" + ANYTHING_PATTERN + "}")
+    @Consumes(WILDCARD)
+    @Produces(APPLICATION_JSON)
+    public Response processNotification(final String body,
+                                        @PathParam(QUERY_PAYMENT_PLUGIN_NAME) final String pluginName,
+                                        @QueryParam(QUERY_PLUGIN_PROPERTY) final List<String> pluginPropertiesString,
+                                        @HeaderParam(HDR_CREATED_BY) final String createdBy,
+                                        @HeaderParam(HDR_REASON) final String reason,
+                                        @HeaderParam(HDR_COMMENT) final String comment,
+                                        @javax.ws.rs.core.Context final UriInfo uriInfo,
+                                        @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException {
+        final Iterable<PluginProperty> pluginProperties = extractPluginProperties(pluginPropertiesString);
+        final CallContext callContext = context.createContext(createdBy, reason, comment, request);
+
+        // Note: the body is opaque here, as it comes from the gateway. The associated payment plugin will know how to deserialize it though
+        final GatewayNotification notification = paymentGatewayApi.processNotification(body, pluginName, pluginProperties, callContext);
+        final GatewayNotificationJson result = new GatewayNotificationJson(notification);
+
+        // The plugin told us how to build the response
+        return result.toResponse();
+    }
+}
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentResource.java
index 7c918d4..7c59b72 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/PaymentResource.java
@@ -52,7 +52,6 @@ import org.killbill.billing.invoice.api.InvoicePayment;
 import org.killbill.billing.invoice.api.InvoicePaymentApi;
 import org.killbill.billing.jaxrs.json.ChargebackJson;
 import org.killbill.billing.jaxrs.json.CustomFieldJson;
-import org.killbill.billing.jaxrs.json.HostedPaymentPageFieldsJson;
 import org.killbill.billing.jaxrs.json.InvoiceItemJson;
 import org.killbill.billing.jaxrs.json.PaymentJson;
 import org.killbill.billing.jaxrs.json.RefundJson;
@@ -82,7 +81,6 @@ import com.google.common.collect.ImmutableMap;
 import com.google.inject.Inject;
 
 import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
-import static javax.ws.rs.core.MediaType.WILDCARD;
 
 @Path(JaxrsResource.PAYMENTS_PATH)
 public class PaymentResource extends JaxRsResourceBase {
@@ -320,36 +318,6 @@ public class PaymentResource extends JaxRsResourceBase {
         return uriBuilder.buildResponse(RefundResource.class, "getRefund", result.getId(), uriInfo.getBaseUri().toString());
     }
 
-    @POST
-    @Path("/" + HOSTED + "/" + FORM + "/{gateway:" + ANYTHING_PATTERN + "}")
-    @Consumes(APPLICATION_JSON)
-    @Produces(APPLICATION_JSON)
-    // Generate form data to redirect the customer to the gateway
-    public Response buildFormDescriptor(final HostedPaymentPageFieldsJson json,
-                                        @PathParam("gateway") final String gateway,
-                                        @HeaderParam(HDR_CREATED_BY) final String createdBy,
-                                        @HeaderParam(HDR_REASON) final String reason,
-                                        @HeaderParam(HDR_COMMENT) final String comment,
-                                        @javax.ws.rs.core.Context final UriInfo uriInfo,
-                                        @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException {
-        throw new UnsupportedOperationException();
-    }
-
-    @POST
-    @Path("/" + HOSTED)
-    @Consumes(WILDCARD)
-    @Produces(APPLICATION_JSON)
-    public Response processNotification(final String body,
-                                        @PathParam("gateway") final String gateway,
-                                        @HeaderParam(HDR_CREATED_BY) final String createdBy,
-                                        @HeaderParam(HDR_REASON) final String reason,
-                                        @HeaderParam(HDR_COMMENT) final String comment,
-                                        @javax.ws.rs.core.Context final UriInfo uriInfo,
-                                        @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException {
-        // Note: the body is opaque here, as it comes from the gateway. The associated payment plugin will now how to deserialize it though
-        throw new UnsupportedOperationException();
-    }
-
     @GET
     @Path("/{paymentId:" + UUID_PATTERN + "}/" + CUSTOM_FIELDS)
     @Produces(APPLICATION_JSON)
diff --git a/osgi-bundles/bundles/jruby/src/main/java/org/killbill/billing/osgi/bundles/jruby/JRubyPaymentPlugin.java b/osgi-bundles/bundles/jruby/src/main/java/org/killbill/billing/osgi/bundles/jruby/JRubyPaymentPlugin.java
index 1d4fd34..8443126 100644
--- a/osgi-bundles/bundles/jruby/src/main/java/org/killbill/billing/osgi/bundles/jruby/JRubyPaymentPlugin.java
+++ b/osgi-bundles/bundles/jruby/src/main/java/org/killbill/billing/osgi/bundles/jruby/JRubyPaymentPlugin.java
@@ -30,9 +30,8 @@ import org.killbill.billing.osgi.api.OSGIPluginProperties;
 import org.killbill.billing.osgi.api.config.PluginRubyConfig;
 import org.killbill.billing.payment.api.PaymentMethodPlugin;
 import org.killbill.billing.payment.api.PluginProperty;
-import org.killbill.billing.payment.plugin.api.HostedPaymentPageDescriptorFields;
+import org.killbill.billing.payment.plugin.api.GatewayNotification;
 import org.killbill.billing.payment.plugin.api.HostedPaymentPageFormDescriptor;
-import org.killbill.billing.payment.plugin.api.HostedPaymentPageNotification;
 import org.killbill.billing.payment.plugin.api.PaymentInfoPlugin;
 import org.killbill.billing.payment.plugin.api.PaymentMethodInfoPlugin;
 import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
@@ -237,20 +236,20 @@ public class JRubyPaymentPlugin extends JRubyPlugin implements PaymentPluginApi 
     }
 
     @Override
-    public HostedPaymentPageFormDescriptor buildFormDescriptor(final UUID kbAccountId, final HostedPaymentPageDescriptorFields hostedPaymentPageDescriptorFields, final Iterable<PluginProperty> properties, final TenantContext context) throws PaymentPluginApiException {
+    public HostedPaymentPageFormDescriptor buildFormDescriptor(final UUID kbAccountId, final Iterable<PluginProperty> customFields, final Iterable<PluginProperty> properties, final CallContext context) throws PaymentPluginApiException {
         return callWithRuntimeAndChecking(new PluginCallback<HostedPaymentPageFormDescriptor>(VALIDATION_PLUGIN_TYPE.PAYMENT) {
             @Override
             public HostedPaymentPageFormDescriptor doCall(final Ruby runtime) throws PaymentPluginApiException {
-                return ((PaymentPluginApi) pluginInstance).buildFormDescriptor(kbAccountId, hostedPaymentPageDescriptorFields, properties, context);
+                return ((PaymentPluginApi) pluginInstance).buildFormDescriptor(kbAccountId, customFields, properties, context);
             }
         });
     }
 
     @Override
-    public HostedPaymentPageNotification processNotification(final String notification, final Iterable<PluginProperty> properties, final TenantContext context) throws PaymentPluginApiException {
-        return callWithRuntimeAndChecking(new PluginCallback<HostedPaymentPageNotification>(VALIDATION_PLUGIN_TYPE.PAYMENT) {
+    public GatewayNotification processNotification(final String notification, final Iterable<PluginProperty> properties, final CallContext context) throws PaymentPluginApiException {
+        return callWithRuntimeAndChecking(new PluginCallback<GatewayNotification>(VALIDATION_PLUGIN_TYPE.PAYMENT) {
             @Override
-            public HostedPaymentPageNotification doCall(final Ruby runtime) throws PaymentPluginApiException {
+            public GatewayNotification doCall(final Ruby runtime) throws PaymentPluginApiException {
                 return ((PaymentPluginApi) pluginInstance).processNotification(notification, properties, context);
             }
         });
diff --git a/osgi-bundles/tests/beatrix/src/test/java/org/killbill/billing/osgi/bundles/test/TestPaymentPluginApi.java b/osgi-bundles/tests/beatrix/src/test/java/org/killbill/billing/osgi/bundles/test/TestPaymentPluginApi.java
index fec09e0..3c04998 100644
--- a/osgi-bundles/tests/beatrix/src/test/java/org/killbill/billing/osgi/bundles/test/TestPaymentPluginApi.java
+++ b/osgi-bundles/tests/beatrix/src/test/java/org/killbill/billing/osgi/bundles/test/TestPaymentPluginApi.java
@@ -30,9 +30,8 @@ import org.killbill.billing.catalog.api.Currency;
 import org.killbill.billing.osgi.bundles.test.dao.TestDao;
 import org.killbill.billing.payment.api.PaymentMethodPlugin;
 import org.killbill.billing.payment.api.PluginProperty;
-import org.killbill.billing.payment.plugin.api.HostedPaymentPageDescriptorFields;
+import org.killbill.billing.payment.plugin.api.GatewayNotification;
 import org.killbill.billing.payment.plugin.api.HostedPaymentPageFormDescriptor;
-import org.killbill.billing.payment.plugin.api.HostedPaymentPageNotification;
 import org.killbill.billing.payment.plugin.api.PaymentInfoPlugin;
 import org.killbill.billing.payment.plugin.api.PaymentMethodInfoPlugin;
 import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
@@ -261,12 +260,12 @@ public class TestPaymentPluginApi implements PaymentPluginApi {
     }
 
     @Override
-    public HostedPaymentPageFormDescriptor buildFormDescriptor(final UUID kbAccountId, final HostedPaymentPageDescriptorFields hostedPaymentPageDescriptorFields, final Iterable<PluginProperty> properties, final TenantContext tenantContext) {
+    public HostedPaymentPageFormDescriptor buildFormDescriptor(final UUID kbAccountId, final Iterable<PluginProperty> customFields, final Iterable<PluginProperty> properties, final CallContext callContext) {
         return null;
     }
 
     @Override
-    public HostedPaymentPageNotification processNotification(final String notification, final Iterable<PluginProperty> properties, final TenantContext tenantContext) throws PaymentPluginApiException {
+    public GatewayNotification processNotification(final String notification, final Iterable<PluginProperty> properties, final CallContext callContext) throws PaymentPluginApiException {
         return null;
     }
 }
diff --git a/osgi-bundles/tests/payment/src/test/java/org/killbill/billing/osgi/bundles/test/TestPaymentPluginApi.java b/osgi-bundles/tests/payment/src/test/java/org/killbill/billing/osgi/bundles/test/TestPaymentPluginApi.java
index 5d391cf..94cff65 100644
--- a/osgi-bundles/tests/payment/src/test/java/org/killbill/billing/osgi/bundles/test/TestPaymentPluginApi.java
+++ b/osgi-bundles/tests/payment/src/test/java/org/killbill/billing/osgi/bundles/test/TestPaymentPluginApi.java
@@ -28,9 +28,8 @@ import org.joda.time.DateTime;
 import org.killbill.billing.catalog.api.Currency;
 import org.killbill.billing.payment.api.PaymentMethodPlugin;
 import org.killbill.billing.payment.api.PluginProperty;
-import org.killbill.billing.payment.plugin.api.HostedPaymentPageDescriptorFields;
+import org.killbill.billing.payment.plugin.api.GatewayNotification;
 import org.killbill.billing.payment.plugin.api.HostedPaymentPageFormDescriptor;
-import org.killbill.billing.payment.plugin.api.HostedPaymentPageNotification;
 import org.killbill.billing.payment.plugin.api.PaymentInfoPlugin;
 import org.killbill.billing.payment.plugin.api.PaymentMethodInfoPlugin;
 import org.killbill.billing.payment.plugin.api.PaymentPluginApiException;
@@ -328,12 +327,12 @@ public class TestPaymentPluginApi implements PaymentPluginApiWithTestControl {
     }
 
     @Override
-    public HostedPaymentPageFormDescriptor buildFormDescriptor(final UUID kbAccountId, final HostedPaymentPageDescriptorFields hostedPaymentPageDescriptorFields, final Iterable<PluginProperty> properties, final TenantContext tenantContext) {
+    public HostedPaymentPageFormDescriptor buildFormDescriptor(final UUID kbAccountId, final Iterable<PluginProperty> customFields, final Iterable<PluginProperty> properties, final CallContext callContext) {
         return null;
     }
 
     @Override
-    public HostedPaymentPageNotification processNotification(final String notification, final Iterable<PluginProperty> properties, final TenantContext tenantContext) throws PaymentPluginApiException {
+    public GatewayNotification processNotification(final String notification, final Iterable<PluginProperty> properties, final CallContext callContext) throws PaymentPluginApiException {
         return null;
     }
 
diff --git a/payment/src/main/java/org/killbill/billing/payment/api/svcs/DefaultPaymentGatewayApi.java b/payment/src/main/java/org/killbill/billing/payment/api/svcs/DefaultPaymentGatewayApi.java
new file mode 100644
index 0000000..b27a38c
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/api/svcs/DefaultPaymentGatewayApi.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2014 Groupon, Inc
+ * Copyright 2014 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.payment.api.svcs;
+
+import javax.inject.Inject;
+
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.payment.api.PaymentApiException;
+import org.killbill.billing.payment.api.PaymentGatewayApi;
+import org.killbill.billing.payment.api.PluginProperty;
+import org.killbill.billing.payment.core.PaymentGatewayProcessor;
+import org.killbill.billing.payment.plugin.api.GatewayNotification;
+import org.killbill.billing.payment.plugin.api.HostedPaymentPageFormDescriptor;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+
+public class DefaultPaymentGatewayApi implements PaymentGatewayApi {
+
+    private final PaymentGatewayProcessor paymentGatewayProcessor;
+    private final InternalCallContextFactory internalCallContextFactory;
+
+    @Inject
+    public DefaultPaymentGatewayApi(final PaymentGatewayProcessor paymentGatewayProcessor, final InternalCallContextFactory internalCallContextFactory) {
+        this.paymentGatewayProcessor = paymentGatewayProcessor;
+        this.internalCallContextFactory = internalCallContextFactory;
+    }
+
+    @Override
+    public HostedPaymentPageFormDescriptor buildFormDescriptor(final Account account, final Iterable<PluginProperty> customFields, final Iterable<PluginProperty> properties, final CallContext callContext) throws PaymentApiException {
+        return paymentGatewayProcessor.buildFormDescriptor(account, customFields, properties, internalCallContextFactory.createInternalCallContext(account.getId(), callContext), callContext);
+    }
+
+    @Override
+    public GatewayNotification processNotification(final String notification, final String pluginName, final Iterable<PluginProperty> properties, final CallContext callContext) throws PaymentApiException {
+        return paymentGatewayProcessor.processNotification(notification, pluginName, properties, callContext);
+    }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/DirectPaymentProcessor.java b/payment/src/main/java/org/killbill/billing/payment/core/DirectPaymentProcessor.java
index a157ce1..e2b6283 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/DirectPaymentProcessor.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/DirectPaymentProcessor.java
@@ -367,38 +367,38 @@ public class DirectPaymentProcessor extends ProcessorBase {
         Preconditions.checkArgument(account.getCurrency().equals(currency), String.format("Currency %s doesn't match the one on the account (%s)", currency, currency));
 
         try {
-            return paymentPluginDispatcher.dispatchWithAccountLock(new CallableWithAccountLock<DirectPayment>(locker,
-                                                                                                              account.getExternalKey(),
-                                                                                                              new WithAccountLockCallback<DirectPayment>() {
-
-                                                                                                                  @Override
-                                                                                                                  public DirectPayment doOperation() throws PaymentApiException {
-                                                                                                                      final DateTime utcNow = clock.getUTCNow();
-                                                                                                                      final DirectPaymentModelDao paymentModelDao;
-                                                                                                                      final DirectPaymentTransactionModelDao paymentTransactionModelDao;
-                                                                                                                      if (directPaymentId == null) {
-                                                                                                                          final DirectPaymentModelDao pmd = new DirectPaymentModelDao(utcNow, utcNow, account.getId(), account.getPaymentMethodId(), externalKey);
-                                                                                                                          final DirectPaymentTransactionModelDao ptmd = new DirectPaymentTransactionModelDao(utcNow, utcNow, pmd.getId(),
-                                                                                                                                                                                                             transactionType, utcNow, PaymentStatus.UNKNOWN,
-                                                                                                                                                                                                             amount, currency, null, null);
-
-                                                                                                                          paymentModelDao = paymentDao.insertDirectPaymentWithFirstTransaction(pmd, ptmd, callContext);
-                                                                                                                          paymentTransactionModelDao = paymentDao.getDirectTransactionsForAccount(account.getId(), callContext).get(0);
-                                                                                                                      } else {
-                                                                                                                          paymentModelDao = paymentDao.getDirectPayment(directPaymentId, callContext);
-                                                                                                                          if (paymentModelDao == null) {
-                                                                                                                              throw new PaymentApiException(ErrorCode.PAYMENT_NO_SUCH_PAYMENT, directPaymentId);
-                                                                                                                          }
-
-                                                                                                                          final DirectPaymentTransactionModelDao ptmd = new DirectPaymentTransactionModelDao(utcNow, utcNow, directPaymentId,
-                                                                                                                                                                                                             transactionType, utcNow, PaymentStatus.UNKNOWN,
-                                                                                                                                                                                                             amount, currency, null, null);
-                                                                                                                          paymentTransactionModelDao = paymentDao.updateDirectPaymentWithNewTransaction(directPaymentId, ptmd, callContext);
+            return paymentPluginDispatcher.dispatchWithTimeout(new CallableWithAccountLock<DirectPayment>(locker,
+                                                                                                          account.getExternalKey(),
+                                                                                                          new WithAccountLockCallback<DirectPayment>() {
+
+                                                                                                              @Override
+                                                                                                              public DirectPayment doOperation() throws PaymentApiException {
+                                                                                                                  final DateTime utcNow = clock.getUTCNow();
+                                                                                                                  final DirectPaymentModelDao paymentModelDao;
+                                                                                                                  final DirectPaymentTransactionModelDao paymentTransactionModelDao;
+                                                                                                                  if (directPaymentId == null) {
+                                                                                                                      final DirectPaymentModelDao pmd = new DirectPaymentModelDao(utcNow, utcNow, account.getId(), account.getPaymentMethodId(), externalKey);
+                                                                                                                      final DirectPaymentTransactionModelDao ptmd = new DirectPaymentTransactionModelDao(utcNow, utcNow, pmd.getId(),
+                                                                                                                                                                                                         transactionType, utcNow, PaymentStatus.UNKNOWN,
+                                                                                                                                                                                                         amount, currency, null, null);
+
+                                                                                                                      paymentModelDao = paymentDao.insertDirectPaymentWithFirstTransaction(pmd, ptmd, callContext);
+                                                                                                                      paymentTransactionModelDao = paymentDao.getDirectTransactionsForAccount(account.getId(), callContext).get(0);
+                                                                                                                  } else {
+                                                                                                                      paymentModelDao = paymentDao.getDirectPayment(directPaymentId, callContext);
+                                                                                                                      if (paymentModelDao == null) {
+                                                                                                                          throw new PaymentApiException(ErrorCode.PAYMENT_NO_SUCH_PAYMENT, directPaymentId);
                                                                                                                       }
 
-                                                                                                                      return getDirectPayment(pluginWrapper, account, amount, currency, paymentModelDao.getId(), paymentTransactionModelDao.getId(), properties, callContext);
+                                                                                                                      final DirectPaymentTransactionModelDao ptmd = new DirectPaymentTransactionModelDao(utcNow, utcNow, directPaymentId,
+                                                                                                                                                                                                         transactionType, utcNow, PaymentStatus.UNKNOWN,
+                                                                                                                                                                                                         amount, currency, null, null);
+                                                                                                                      paymentTransactionModelDao = paymentDao.updateDirectPaymentWithNewTransaction(directPaymentId, ptmd, callContext);
                                                                                                                   }
+
+                                                                                                                  return getDirectPayment(pluginWrapper, account, amount, currency, paymentModelDao.getId(), paymentTransactionModelDao.getId(), properties, callContext);
                                                                                                               }
+                                                                                                          }
             ));
         } catch (final TimeoutException e) {
             // TODO PIERRE
@@ -414,21 +414,21 @@ public class DirectPaymentProcessor extends ProcessorBase {
         Preconditions.checkArgument((amount == null && currency == null) || account.getCurrency().equals(currency), String.format("Currency %s doesn't match the one on the account (%s)", currency, currency));
 
         try {
-            return paymentPluginDispatcher.dispatchWithAccountLock(new CallableWithAccountLock<DirectPayment>(locker,
-                                                                                                              account.getExternalKey(),
-                                                                                                              new WithAccountLockCallback<DirectPayment>() {
-
-                                                                                                                  @Override
-                                                                                                                  public DirectPayment doOperation() throws PaymentApiException {
-                                                                                                                      final DateTime utcNow = clock.getUTCNow();
-                                                                                                                      final DirectPaymentTransactionModelDao ptmd = new DirectPaymentTransactionModelDao(utcNow, utcNow, directPaymentId,
-                                                                                                                                                                                                         transactionType, utcNow, PaymentStatus.UNKNOWN,
-                                                                                                                                                                                                         amount, currency, null, null);
-                                                                                                                      final DirectPaymentTransactionModelDao inserted = paymentDao.updateDirectPaymentWithNewTransaction(directPaymentId, ptmd, callContext);
-
-                                                                                                                      return getDirectPayment(pluginWrapper, account, amount, currency, directPaymentId, inserted.getId(), properties, callContext);
-                                                                                                                  }
+            return paymentPluginDispatcher.dispatchWithTimeout(new CallableWithAccountLock<DirectPayment>(locker,
+                                                                                                          account.getExternalKey(),
+                                                                                                          new WithAccountLockCallback<DirectPayment>() {
+
+                                                                                                              @Override
+                                                                                                              public DirectPayment doOperation() throws PaymentApiException {
+                                                                                                                  final DateTime utcNow = clock.getUTCNow();
+                                                                                                                  final DirectPaymentTransactionModelDao ptmd = new DirectPaymentTransactionModelDao(utcNow, utcNow, directPaymentId,
+                                                                                                                                                                                                     transactionType, utcNow, PaymentStatus.UNKNOWN,
+                                                                                                                                                                                                     amount, currency, null, null);
+                                                                                                                  final DirectPaymentTransactionModelDao inserted = paymentDao.updateDirectPaymentWithNewTransaction(directPaymentId, ptmd, callContext);
+
+                                                                                                                  return getDirectPayment(pluginWrapper, account, amount, currency, directPaymentId, inserted.getId(), properties, callContext);
                                                                                                               }
+                                                                                                          }
             ));
         } catch (final TimeoutException e) {
             // TODO PIERRE
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/PaymentGatewayProcessor.java b/payment/src/main/java/org/killbill/billing/payment/core/PaymentGatewayProcessor.java
new file mode 100644
index 0000000..695c7ca
--- /dev/null
+++ b/payment/src/main/java/org/killbill/billing/payment/core/PaymentGatewayProcessor.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2014 Groupon, Inc
+ * Copyright 2014 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.payment.core;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import javax.inject.Inject;
+
+import org.killbill.billing.ErrorCode;
+import org.killbill.billing.account.api.Account;
+import org.killbill.billing.account.api.AccountInternalApi;
+import org.killbill.billing.callcontext.InternalCallContext;
+import org.killbill.billing.invoice.api.InvoiceInternalApi;
+import org.killbill.billing.osgi.api.OSGIServiceRegistration;
+import org.killbill.billing.payment.api.PaymentApiException;
+import org.killbill.billing.payment.api.PluginProperty;
+import org.killbill.billing.payment.dao.PaymentDao;
+import org.killbill.billing.payment.dispatcher.PluginDispatcher;
+import org.killbill.billing.payment.plugin.api.GatewayNotification;
+import org.killbill.billing.payment.plugin.api.HostedPaymentPageFormDescriptor;
+import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
+import org.killbill.billing.payment.plugin.api.PaymentPluginApiException;
+import org.killbill.billing.tag.TagInternalApi;
+import org.killbill.billing.util.callcontext.CallContext;
+import org.killbill.billing.util.config.PaymentConfig;
+import org.killbill.billing.util.dao.NonEntityDao;
+import org.killbill.bus.api.PersistentBus;
+import org.killbill.commons.locker.GlobalLocker;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.inject.name.Named;
+
+import static org.killbill.billing.payment.glue.PaymentModule.PLUGIN_EXECUTOR_NAMED;
+
+public class PaymentGatewayProcessor extends ProcessorBase {
+
+    private final PluginDispatcher<HostedPaymentPageFormDescriptor> paymentPluginFormDispatcher;
+    private final PluginDispatcher<GatewayNotification> paymentPluginNotificationDispatcher;
+
+    private static final Logger log = LoggerFactory.getLogger(PaymentGatewayProcessor.class);
+
+    @Inject
+    public PaymentGatewayProcessor(final OSGIServiceRegistration<PaymentPluginApi> pluginRegistry,
+                                   final AccountInternalApi accountUserApi,
+                                   final InvoiceInternalApi invoiceApi,
+                                   final TagInternalApi tagUserApi,
+                                   final PaymentDao paymentDao,
+                                   final NonEntityDao nonEntityDao,
+                                   final PersistentBus eventBus,
+                                   final GlobalLocker locker,
+                                   final PaymentConfig paymentConfig,
+                                   @Named(PLUGIN_EXECUTOR_NAMED) final ExecutorService executor) {
+        super(pluginRegistry, accountUserApi, eventBus, paymentDao, nonEntityDao, tagUserApi, locker, executor, invoiceApi);
+        final long paymentPluginTimeoutSec = TimeUnit.SECONDS.convert(paymentConfig.getPaymentPluginTimeout().getPeriod(), paymentConfig.getPaymentPluginTimeout().getUnit());
+        this.paymentPluginFormDispatcher = new PluginDispatcher<HostedPaymentPageFormDescriptor>(paymentPluginTimeoutSec, executor);
+        this.paymentPluginNotificationDispatcher = new PluginDispatcher<GatewayNotification>(paymentPluginTimeoutSec, executor);
+    }
+
+    public HostedPaymentPageFormDescriptor buildFormDescriptor(final Account account, final Iterable<PluginProperty> customFields, final Iterable<PluginProperty> properties, final InternalCallContext internalCallContext, final CallContext callContext) throws PaymentApiException {
+        try {
+            return paymentPluginFormDispatcher.dispatchWithTimeout(new CallableWithAccountLock<HostedPaymentPageFormDescriptor>(locker,
+                                                                                                                                account.getExternalKey(),
+                                                                                                                                new WithAccountLockCallback<HostedPaymentPageFormDescriptor>() {
+
+                                                                                                                                    @Override
+                                                                                                                                    public HostedPaymentPageFormDescriptor doOperation() throws PaymentApiException {
+                                                                                                                                        final PaymentPluginApi plugin = getPaymentProviderPlugin(account, internalCallContext);
+
+                                                                                                                                        try {
+                                                                                                                                            return plugin.buildFormDescriptor(account.getId(), customFields, properties, callContext);
+                                                                                                                                        } catch (final RuntimeException e) {
+                                                                                                                                            throw new PaymentApiException(e, ErrorCode.PAYMENT_INTERNAL_ERROR);
+                                                                                                                                        } catch (final PaymentPluginApiException e) {
+                                                                                                                                            throw new PaymentApiException(e, ErrorCode.PAYMENT_INTERNAL_ERROR);
+                                                                                                                                        }
+                                                                                                                                    }
+                                                                                                                                }
+            ));
+        } catch (final TimeoutException e) {
+            throw new PaymentApiException(ErrorCode.PAYMENT_PLUGIN_TIMEOUT, account.getId(), null);
+        } catch (final RuntimeException e) {
+            throw new PaymentApiException(ErrorCode.PAYMENT_INTERNAL_ERROR, null);
+        }
+    }
+
+    public GatewayNotification processNotification(final String notification, final String pluginName, final Iterable<PluginProperty> properties, final CallContext callContext) throws PaymentApiException {
+        try {
+            return paymentPluginNotificationDispatcher.dispatchWithTimeout(new Callable<GatewayNotification>() {
+                                                                               @Override
+                                                                               public GatewayNotification call() throws Exception {
+                                                                                   final PaymentPluginApi plugin = getPaymentPluginApi(pluginName);
+                                                                                   return plugin.processNotification(notification, properties, callContext);
+                                                                               }
+                                                                           }
+                                                                          );
+        } catch (final TimeoutException e) {
+            throw new PaymentApiException(ErrorCode.PAYMENT_PLUGIN_TIMEOUT, null, null);
+        } catch (final RuntimeException e) {
+            throw new PaymentApiException(ErrorCode.PAYMENT_INTERNAL_ERROR, null);
+        }
+    }
+}
diff --git a/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java b/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java
index 0eb2783..b0fc1d1 100644
--- a/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java
+++ b/payment/src/main/java/org/killbill/billing/payment/core/PaymentProcessor.java
@@ -269,46 +269,46 @@ public class PaymentProcessor extends ProcessorBase {
     public void process_AUTO_PAY_OFF_removal(final Account account, final InternalCallContext context) throws PaymentApiException {
 
         try {
-            voidPluginDispatcher.dispatchWithAccountLock(new CallableWithAccountLock<Void>(locker,
-                                                                                           account.getExternalKey(),
-                                                                                           new WithAccountLockCallback<Void>() {
-
-                                                                                               @Override
-                                                                                               public Void doOperation() throws PaymentApiException {
-
-                                                                                                   final List<PaymentModelDao> payments = paymentDao.getPaymentsForAccount(account.getId(), context);
-                                                                                                   final Collection<PaymentModelDao> paymentsToBeCompleted = Collections2.filter(payments, new Predicate<PaymentModelDao>() {
-                                                                                                       @Override
-                                                                                                       public boolean apply(final PaymentModelDao in) {
-                                                                                                           // Payments left in AUTO_PAY_OFF or for which we did not retry enough
-                                                                                                           return (in.getPaymentStatus() == PaymentStatus.AUTO_PAY_OFF ||
-                                                                                                                   in.getPaymentStatus() == PaymentStatus.PAYMENT_FAILURE ||
-                                                                                                                   in.getPaymentStatus() == PaymentStatus.PLUGIN_FAILURE ||
-                                                                                                                   in.getPaymentStatus() == PaymentStatus.UNKNOWN);
-                                                                                                       }
-                                                                                                   });
-                                                                                                   // Insert one retry event for each payment left in AUTO_PAY_OFF
-                                                                                                   for (final PaymentModelDao cur : paymentsToBeCompleted) {
-                                                                                                       switch (cur.getPaymentStatus()) {
-                                                                                                           case AUTO_PAY_OFF:
-                                                                                                               autoPayoffRetryService.scheduleRetry(cur.getId(), clock.getUTCNow());
-                                                                                                               break;
-                                                                                                           case PAYMENT_FAILURE:
-                                                                                                               scheduleRetryOnPaymentFailure(cur.getId(), context);
-                                                                                                               break;
-                                                                                                           case PLUGIN_FAILURE:
-                                                                                                           case UNKNOWN:
-                                                                                                               scheduleRetryOnPluginFailure(cur.getId(), context);
-                                                                                                               break;
-                                                                                                           default:
-                                                                                                               // Impossible...
-                                                                                                               throw new RuntimeException("Unexpected case " + cur.getPaymentStatus());
-                                                                                                       }
-
+            voidPluginDispatcher.dispatchWithTimeout(new CallableWithAccountLock<Void>(locker,
+                                                                                       account.getExternalKey(),
+                                                                                       new WithAccountLockCallback<Void>() {
+
+                                                                                           @Override
+                                                                                           public Void doOperation() throws PaymentApiException {
+
+                                                                                               final List<PaymentModelDao> payments = paymentDao.getPaymentsForAccount(account.getId(), context);
+                                                                                               final Collection<PaymentModelDao> paymentsToBeCompleted = Collections2.filter(payments, new Predicate<PaymentModelDao>() {
+                                                                                                   @Override
+                                                                                                   public boolean apply(final PaymentModelDao in) {
+                                                                                                       // Payments left in AUTO_PAY_OFF or for which we did not retry enough
+                                                                                                       return (in.getPaymentStatus() == PaymentStatus.AUTO_PAY_OFF ||
+                                                                                                               in.getPaymentStatus() == PaymentStatus.PAYMENT_FAILURE ||
+                                                                                                               in.getPaymentStatus() == PaymentStatus.PLUGIN_FAILURE ||
+                                                                                                               in.getPaymentStatus() == PaymentStatus.UNKNOWN);
                                                                                                    }
-                                                                                                   return null;
+                                                                                               });
+                                                                                               // Insert one retry event for each payment left in AUTO_PAY_OFF
+                                                                                               for (final PaymentModelDao cur : paymentsToBeCompleted) {
+                                                                                                   switch (cur.getPaymentStatus()) {
+                                                                                                       case AUTO_PAY_OFF:
+                                                                                                           autoPayoffRetryService.scheduleRetry(cur.getId(), clock.getUTCNow());
+                                                                                                           break;
+                                                                                                       case PAYMENT_FAILURE:
+                                                                                                           scheduleRetryOnPaymentFailure(cur.getId(), context);
+                                                                                                           break;
+                                                                                                       case PLUGIN_FAILURE:
+                                                                                                       case UNKNOWN:
+                                                                                                           scheduleRetryOnPluginFailure(cur.getId(), context);
+                                                                                                           break;
+                                                                                                       default:
+                                                                                                           // Impossible...
+                                                                                                           throw new RuntimeException("Unexpected case " + cur.getPaymentStatus());
+                                                                                                   }
+
                                                                                                }
+                                                                                               return null;
                                                                                            }
+                                                                                       }
             ));
         } catch (final TimeoutException e) {
             throw new PaymentApiException(ErrorCode.UNEXPECTED_ERROR, "Unexpected timeout for payment creation (AUTO_PAY_OFF)");
@@ -332,65 +332,65 @@ public class PaymentProcessor extends ProcessorBase {
         }
 
         try {
-            return paymentPluginDispatcher.dispatchWithAccountLock(new CallableWithAccountLock<Payment>(locker,
-                                                                                                        account.getExternalKey(),
-                                                                                                        new WithAccountLockCallback<Payment>() {
+            return paymentPluginDispatcher.dispatchWithTimeout(new CallableWithAccountLock<Payment>(locker,
+                                                                                                    account.getExternalKey(),
+                                                                                                    new WithAccountLockCallback<Payment>() {
+
+                                                                                                        @Override
+                                                                                                        public Payment doOperation() throws PaymentApiException {
+
+                                                                                                            try {
+                                                                                                                // First, rebalance CBA and retrieve the latest version of the invoice
+                                                                                                                final Invoice invoice = rebalanceAndGetInvoice(account.getId(), invoiceId, context);
+                                                                                                                if (invoice == null || invoice.isMigrationInvoice()) {
+                                                                                                                    log.error("Received invoice for payment that is a migration invoice - don't know how to handle those yet: {}", invoice);
+                                                                                                                    return null;
+                                                                                                                }
 
-                                                                                                            @Override
-                                                                                                            public Payment doOperation() throws PaymentApiException {
+                                                                                                                // Second, validate the payment amount. We want to bail as early as possible if e.g. the balance is zero
+                                                                                                                final BigDecimal requestedAmount = getAndValidatePaymentAmount(invoice, inputAmount, isInstantPayment);
 
+                                                                                                                // Third, retrieve the payment method and associated plugin
+                                                                                                                final PaymentPluginApi plugin;
+                                                                                                                final UUID paymentMethodId;
                                                                                                                 try {
-                                                                                                                    // First, rebalance CBA and retrieve the latest version of the invoice
-                                                                                                                    final Invoice invoice = rebalanceAndGetInvoice(account.getId(), invoiceId, context);
-                                                                                                                    if (invoice == null || invoice.isMigrationInvoice()) {
-                                                                                                                        log.error("Received invoice for payment that is a migration invoice - don't know how to handle those yet: {}", invoice);
-                                                                                                                        return null;
-                                                                                                                    }
-
-                                                                                                                    // Second, validate the payment amount. We want to bail as early as possible if e.g. the balance is zero
-                                                                                                                    final BigDecimal requestedAmount = getAndValidatePaymentAmount(invoice, inputAmount, isInstantPayment);
-
-                                                                                                                    // Third, retrieve the payment method and associated plugin
-                                                                                                                    final PaymentPluginApi plugin;
-                                                                                                                    final UUID paymentMethodId;
-                                                                                                                    try {
-                                                                                                                        // Use the special external payment plugin to handle external payments
-                                                                                                                        if (isExternalPayment) {
-                                                                                                                            plugin = externalPaymentPlugin;
-                                                                                                                            paymentMethodId = paymentMethodProcessor.getExternalPaymentMethod(account, properties, context).getId();
-                                                                                                                        } else {
-                                                                                                                            plugin = getPaymentProviderPlugin(account, context);
-                                                                                                                            paymentMethodId = account.getPaymentMethodId();
-                                                                                                                        }
-                                                                                                                    } catch (final PaymentApiException e) {
-
-                                                                                                                        // Insert a payment entry with one attempt in a terminal state to keep a record of the failure
-                                                                                                                        processNewPaymentForMissingDefaultPaymentMethodWithAccountLocked(account, invoice, requestedAmount, context);
-
-                                                                                                                        // This event will be caught by overdue to refresh the overdue state, if needed.
-                                                                                                                        // Note that at this point, we don't know the exact invoice balance (see getAndValidatePaymentAmount() below).
-                                                                                                                        // This means that events will be posted for null and zero dollar invoices (e.g. trials).
-                                                                                                                        final PaymentErrorInternalEvent event = new DefaultPaymentErrorEvent(account.getId(), invoiceId, null,
-                                                                                                                                                                                             ErrorCode.PAYMENT_NO_DEFAULT_PAYMENT_METHOD.toString(),
-                                                                                                                                                                                             context.getAccountRecordId(), context.getTenantRecordId(),
-                                                                                                                                                                                             context.getUserToken());
-                                                                                                                        postPaymentEvent(event, account.getId(), context);
-                                                                                                                        throw e;
+                                                                                                                    // Use the special external payment plugin to handle external payments
+                                                                                                                    if (isExternalPayment) {
+                                                                                                                        plugin = externalPaymentPlugin;
+                                                                                                                        paymentMethodId = paymentMethodProcessor.getExternalPaymentMethod(account, properties, context).getId();
+                                                                                                                    } else {
+                                                                                                                        plugin = getPaymentProviderPlugin(account, context);
+                                                                                                                        paymentMethodId = account.getPaymentMethodId();
                                                                                                                     }
+                                                                                                                } catch (final PaymentApiException e) {
+
+                                                                                                                    // Insert a payment entry with one attempt in a terminal state to keep a record of the failure
+                                                                                                                    processNewPaymentForMissingDefaultPaymentMethodWithAccountLocked(account, invoice, requestedAmount, context);
+
+                                                                                                                    // This event will be caught by overdue to refresh the overdue state, if needed.
+                                                                                                                    // Note that at this point, we don't know the exact invoice balance (see getAndValidatePaymentAmount() below).
+                                                                                                                    // This means that events will be posted for null and zero dollar invoices (e.g. trials).
+                                                                                                                    final PaymentErrorInternalEvent event = new DefaultPaymentErrorEvent(account.getId(), invoiceId, null,
+                                                                                                                                                                                         ErrorCode.PAYMENT_NO_DEFAULT_PAYMENT_METHOD.toString(),
+                                                                                                                                                                                         context.getAccountRecordId(), context.getTenantRecordId(),
+                                                                                                                                                                                         context.getUserToken());
+                                                                                                                    postPaymentEvent(event, account.getId(), context);
+                                                                                                                    throw e;
+                                                                                                                }
 
-                                                                                                                    final boolean isAccountAutoPayOff = isAccountAutoPayOff(account.getId(), context);
-                                                                                                                    setUnsaneAccount_AUTO_PAY_OFFWithAccountLock(account.getId(), paymentMethodId, isAccountAutoPayOff, context, isInstantPayment);
+                                                                                                                final boolean isAccountAutoPayOff = isAccountAutoPayOff(account.getId(), context);
+                                                                                                                setUnsaneAccount_AUTO_PAY_OFFWithAccountLock(account.getId(), paymentMethodId, isAccountAutoPayOff, context, isInstantPayment);
 
-                                                                                                                    if (!isInstantPayment && isAccountAutoPayOff) {
-                                                                                                                        return processNewPaymentForAutoPayOffWithAccountLocked(paymentMethodId, account, invoice, requestedAmount, context);
-                                                                                                                    } else {
-                                                                                                                        return processNewPaymentWithAccountLocked(paymentMethodId, plugin, account, invoice, requestedAmount, isInstantPayment, properties, context);
-                                                                                                                    }
-                                                                                                                } catch (final InvoiceApiException e) {
-                                                                                                                    throw new PaymentApiException(e);
+                                                                                                                if (!isInstantPayment && isAccountAutoPayOff) {
+                                                                                                                    return processNewPaymentForAutoPayOffWithAccountLocked(paymentMethodId, account, invoice, requestedAmount, context);
+                                                                                                                } else {
+                                                                                                                    return processNewPaymentWithAccountLocked(paymentMethodId, plugin, account, invoice, requestedAmount, isInstantPayment, properties, context);
                                                                                                                 }
+                                                                                                            } catch (final InvoiceApiException e) {
+                                                                                                                throw new PaymentApiException(e);
                                                                                                             }
                                                                                                         }
+                                                                                                    }
             ));
         } catch (final TimeoutException e) {
             if (isInstantPayment) {
@@ -510,43 +510,43 @@ public class PaymentProcessor extends ProcessorBase {
             final Account account = accountInternalApi.getAccountById(payment.getAccountId(), context);
             final PaymentPluginApi plugin = getPaymentProviderPlugin(account, context);
 
-            voidPluginDispatcher.dispatchWithAccountLock(new CallableWithAccountLock<Void>(locker,
-                                                                                           account.getExternalKey(),
-                                                                                           new WithAccountLockCallback<Void>() {
-
-                                                                                               @Override
-                                                                                               public Void doOperation() throws PaymentApiException {
-                                                                                                   try {
-                                                                                                       // Fetch again with account lock this time
-                                                                                                       final PaymentModelDao payment = paymentDao.getPayment(paymentId, context);
-                                                                                                       boolean foundExpectedState = false;
-                                                                                                       for (final PaymentStatus cur : expectedPaymentStates) {
-                                                                                                           if (payment.getPaymentStatus() == cur) {
-                                                                                                               foundExpectedState = true;
-                                                                                                               break;
-                                                                                                           }
-                                                                                                       }
-                                                                                                       if (!foundExpectedState) {
-                                                                                                           log.info("Aborted retry for payment {} because it is {} state", paymentId, payment.getPaymentStatus());
-                                                                                                           return null;
+            voidPluginDispatcher.dispatchWithTimeout(new CallableWithAccountLock<Void>(locker,
+                                                                                       account.getExternalKey(),
+                                                                                       new WithAccountLockCallback<Void>() {
+
+                                                                                           @Override
+                                                                                           public Void doOperation() throws PaymentApiException {
+                                                                                               try {
+                                                                                                   // Fetch again with account lock this time
+                                                                                                   final PaymentModelDao payment = paymentDao.getPayment(paymentId, context);
+                                                                                                   boolean foundExpectedState = false;
+                                                                                                   for (final PaymentStatus cur : expectedPaymentStates) {
+                                                                                                       if (payment.getPaymentStatus() == cur) {
+                                                                                                           foundExpectedState = true;
+                                                                                                           break;
                                                                                                        }
+                                                                                                   }
+                                                                                                   if (!foundExpectedState) {
+                                                                                                       log.info("Aborted retry for payment {} because it is {} state", paymentId, payment.getPaymentStatus());
+                                                                                                       return null;
+                                                                                                   }
 
-                                                                                                       final Invoice invoice = rebalanceAndGetInvoice(payment.getAccountId(), payment.getInvoiceId(), context);
-                                                                                                       if (invoice == null || invoice.isMigrationInvoice()) {
-                                                                                                           return null;
-                                                                                                       }
-                                                                                                       if (invoice.getBalance().compareTo(BigDecimal.ZERO) <= 0) {
-                                                                                                           log.info("Aborted retry for payment {} because invoice has been paid", paymentId);
-                                                                                                           setTerminalStateOnRetryWithAccountLocked(account, invoice, payment, invoice.getBalance(), "Paid invoice", context);
-                                                                                                           return null;
-                                                                                                       }
-                                                                                                       processRetryPaymentWithAccountLocked(plugin, account, invoice, payment, invoice.getBalance(), properties, context);
+                                                                                                   final Invoice invoice = rebalanceAndGetInvoice(payment.getAccountId(), payment.getInvoiceId(), context);
+                                                                                                   if (invoice == null || invoice.isMigrationInvoice()) {
                                                                                                        return null;
-                                                                                                   } catch (final InvoiceApiException e) {
-                                                                                                       throw new PaymentApiException(e);
                                                                                                    }
+                                                                                                   if (invoice.getBalance().compareTo(BigDecimal.ZERO) <= 0) {
+                                                                                                       log.info("Aborted retry for payment {} because invoice has been paid", paymentId);
+                                                                                                       setTerminalStateOnRetryWithAccountLocked(account, invoice, payment, invoice.getBalance(), "Paid invoice", context);
+                                                                                                       return null;
+                                                                                                   }
+                                                                                                   processRetryPaymentWithAccountLocked(plugin, account, invoice, payment, invoice.getBalance(), properties, context);
+                                                                                                   return null;
+                                                                                               } catch (final InvoiceApiException e) {
+                                                                                                   throw new PaymentApiException(e);
                                                                                                }
                                                                                            }
+                                                                                       }
             ));
         } catch (final AccountApiException e) {
             log.error(String.format("Failed to retry payment for paymentId %s", paymentId), e);
diff --git a/payment/src/main/java/org/killbill/billing/payment/dispatcher/PluginDispatcher.java b/payment/src/main/java/org/killbill/billing/payment/dispatcher/PluginDispatcher.java
index 6bb30d0..d112b14 100644
--- a/payment/src/main/java/org/killbill/billing/payment/dispatcher/PluginDispatcher.java
+++ b/payment/src/main/java/org/killbill/billing/payment/dispatcher/PluginDispatcher.java
@@ -1,7 +1,9 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014 Groupon, Inc
+ * Copyright 2014 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * 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:
  *
@@ -13,6 +15,7 @@
  * License for the specific language governing permissions and limitations
  * under the License.
  */
+
 package org.killbill.billing.payment.dispatcher;
 
 import java.util.concurrent.Callable;
@@ -22,11 +25,10 @@ import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
 import org.killbill.billing.ErrorCode;
 import org.killbill.billing.payment.api.PaymentApiException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 public class PluginDispatcher<T> {
 
@@ -37,34 +39,30 @@ public class PluginDispatcher<T> {
     private final long timeoutSeconds;
     private final ExecutorService executor;
 
-    public PluginDispatcher(final long tiemoutSeconds, final ExecutorService executor) {
-        this.timeoutSeconds = tiemoutSeconds;
+    public PluginDispatcher(final long timeoutSeconds, final ExecutorService executor) {
+        this.timeoutSeconds = timeoutSeconds;
         this.executor = executor;
     }
 
-
-    public T dispatchWithAccountLock(final Callable<T> task)
-            throws PaymentApiException, TimeoutException {
-        return dispatchWithAccountLockAndTimeout(task, timeoutSeconds, DEEFAULT_PLUGIN_TIMEOUT_UNIT);
+    public T dispatchWithTimeout(final Callable<T> task) throws PaymentApiException, TimeoutException {
+        return dispatchWithTimeout(task, timeoutSeconds, DEEFAULT_PLUGIN_TIMEOUT_UNIT);
     }
 
-    public T dispatchWithAccountLockAndTimeout(final Callable<T> task, final long timeout, final TimeUnit unit)
+    public T dispatchWithTimeout(final Callable<T> task, final long timeout, final TimeUnit unit)
             throws PaymentApiException, TimeoutException {
 
         try {
             final Future<T> future = executor.submit(task);
             return future.get(timeout, unit);
-        } catch (ExecutionException e) {
+        } catch (final ExecutionException e) {
             if (e.getCause() instanceof PaymentApiException) {
                 throw (PaymentApiException) e.getCause();
             } else {
                 throw new PaymentApiException(ErrorCode.PAYMENT_INTERNAL_ERROR, e.getMessage());
             }
-        } catch (InterruptedException e) {
+        } catch (final InterruptedException e) {
             Thread.currentThread().interrupt();
             throw new PaymentApiException(ErrorCode.PAYMENT_INTERNAL_ERROR, e.getMessage());
         }
     }
-
-
 }
diff --git a/payment/src/main/java/org/killbill/billing/payment/glue/PaymentModule.java b/payment/src/main/java/org/killbill/billing/payment/glue/PaymentModule.java
index 397927f..df19e2d 100644
--- a/payment/src/main/java/org/killbill/billing/payment/glue/PaymentModule.java
+++ b/payment/src/main/java/org/killbill/billing/payment/glue/PaymentModule.java
@@ -1,7 +1,9 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014 Groupon, Inc
+ * Copyright 2014 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * 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:
  *
@@ -20,20 +22,20 @@ import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ThreadFactory;
 
-import org.killbill.billing.payment.api.DirectPaymentApi;
-import org.killbill.billing.payment.api.svcs.DefaultDirectPaymentApi;
-import org.killbill.billing.payment.core.DirectPaymentProcessor;
-import org.skife.config.ConfigSource;
-import org.skife.config.ConfigurationObjectFactory;
-
 import org.killbill.billing.osgi.api.OSGIServiceRegistration;
 import org.killbill.billing.payment.api.DefaultPaymentApi;
+import org.killbill.billing.payment.api.DirectPaymentApi;
 import org.killbill.billing.payment.api.PaymentApi;
+import org.killbill.billing.payment.api.PaymentGatewayApi;
 import org.killbill.billing.payment.api.PaymentInternalApi;
 import org.killbill.billing.payment.api.PaymentService;
+import org.killbill.billing.payment.api.svcs.DefaultDirectPaymentApi;
+import org.killbill.billing.payment.api.svcs.DefaultPaymentGatewayApi;
 import org.killbill.billing.payment.api.svcs.DefaultPaymentInternalApi;
 import org.killbill.billing.payment.bus.InvoiceHandler;
 import org.killbill.billing.payment.bus.PaymentTagHandler;
+import org.killbill.billing.payment.core.DirectPaymentProcessor;
+import org.killbill.billing.payment.core.PaymentGatewayProcessor;
 import org.killbill.billing.payment.core.PaymentMethodProcessor;
 import org.killbill.billing.payment.core.PaymentProcessor;
 import org.killbill.billing.payment.core.RefundProcessor;
@@ -47,6 +49,8 @@ import org.killbill.billing.payment.retry.FailedPaymentRetryService.FailedPaymen
 import org.killbill.billing.payment.retry.PluginFailureRetryService;
 import org.killbill.billing.payment.retry.PluginFailureRetryService.PluginFailureRetryServiceScheduler;
 import org.killbill.billing.util.config.PaymentConfig;
+import org.skife.config.ConfigSource;
+import org.skife.config.ConfigurationObjectFactory;
 
 import com.google.inject.AbstractModule;
 import com.google.inject.TypeLiteral;
@@ -93,6 +97,7 @@ public class PaymentModule extends AbstractModule {
         bind(ExecutorService.class).annotatedWith(Names.named(PLUGIN_EXECUTOR_NAMED)).toInstance(pluginExecutorService);
         bind(PaymentProcessor.class).asEagerSingleton();
         bind(DirectPaymentProcessor.class).asEagerSingleton();
+        bind(PaymentGatewayProcessor.class).asEagerSingleton();
         bind(RefundProcessor.class).asEagerSingleton();
         bind(PaymentMethodProcessor.class).asEagerSingleton();
     }
@@ -108,6 +113,7 @@ public class PaymentModule extends AbstractModule {
         bind(PaymentInternalApi.class).to(DefaultPaymentInternalApi.class).asEagerSingleton();
         bind(PaymentApi.class).to(DefaultPaymentApi.class).asEagerSingleton();
         bind(DirectPaymentApi.class).to(DefaultDirectPaymentApi.class).asEagerSingleton();
+        bind(PaymentGatewayApi.class).to(DefaultPaymentGatewayApi.class).asEagerSingleton();
         bind(InvoiceHandler.class).asEagerSingleton();
         bind(PaymentTagHandler.class).asEagerSingleton();
         bind(PaymentService.class).to(DefaultPaymentService.class).asEagerSingleton();
diff --git a/payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpPaymentProviderPlugin.java b/payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpPaymentProviderPlugin.java
index d479074..44d894f 100644
--- a/payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpPaymentProviderPlugin.java
+++ b/payment/src/main/java/org/killbill/billing/payment/provider/DefaultNoOpPaymentProviderPlugin.java
@@ -29,9 +29,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
 import org.killbill.billing.catalog.api.Currency;
 import org.killbill.billing.payment.api.PaymentMethodPlugin;
 import org.killbill.billing.payment.api.PluginProperty;
-import org.killbill.billing.payment.plugin.api.HostedPaymentPageDescriptorFields;
+import org.killbill.billing.payment.plugin.api.GatewayNotification;
 import org.killbill.billing.payment.plugin.api.HostedPaymentPageFormDescriptor;
-import org.killbill.billing.payment.plugin.api.HostedPaymentPageNotification;
 import org.killbill.billing.payment.plugin.api.NoOpPaymentPluginApi;
 import org.killbill.billing.payment.plugin.api.PaymentInfoPlugin;
 import org.killbill.billing.payment.plugin.api.PaymentMethodInfoPlugin;
@@ -230,12 +229,12 @@ public class DefaultNoOpPaymentProviderPlugin implements NoOpPaymentPluginApi {
     }
 
     @Override
-    public HostedPaymentPageFormDescriptor buildFormDescriptor(final UUID kbAccountId, final HostedPaymentPageDescriptorFields hostedPaymentPageDescriptorFields, final Iterable<PluginProperty> properties, final TenantContext tenantContext) {
+    public HostedPaymentPageFormDescriptor buildFormDescriptor(final UUID kbAccountId, final Iterable<PluginProperty> customFields, final Iterable<PluginProperty> properties, final CallContext callContext) {
         return null;
     }
 
     @Override
-    public HostedPaymentPageNotification processNotification(final String notification, final Iterable<PluginProperty> properties, final TenantContext tenantContext) throws PaymentPluginApiException {
+    public GatewayNotification processNotification(final String notification, final Iterable<PluginProperty> properties, final CallContext callContext) throws PaymentPluginApiException {
         return null;
     }
 
diff --git a/payment/src/main/java/org/killbill/billing/payment/provider/ExternalPaymentProviderPlugin.java b/payment/src/main/java/org/killbill/billing/payment/provider/ExternalPaymentProviderPlugin.java
index 045c8d1..3e03551 100644
--- a/payment/src/main/java/org/killbill/billing/payment/provider/ExternalPaymentProviderPlugin.java
+++ b/payment/src/main/java/org/killbill/billing/payment/provider/ExternalPaymentProviderPlugin.java
@@ -26,9 +26,8 @@ import java.util.UUID;
 import org.killbill.billing.catalog.api.Currency;
 import org.killbill.billing.payment.api.PaymentMethodPlugin;
 import org.killbill.billing.payment.api.PluginProperty;
-import org.killbill.billing.payment.plugin.api.HostedPaymentPageDescriptorFields;
+import org.killbill.billing.payment.plugin.api.GatewayNotification;
 import org.killbill.billing.payment.plugin.api.HostedPaymentPageFormDescriptor;
-import org.killbill.billing.payment.plugin.api.HostedPaymentPageNotification;
 import org.killbill.billing.payment.plugin.api.PaymentInfoPlugin;
 import org.killbill.billing.payment.plugin.api.PaymentMethodInfoPlugin;
 import org.killbill.billing.payment.plugin.api.PaymentPluginApi;
@@ -140,12 +139,12 @@ public class ExternalPaymentProviderPlugin implements PaymentPluginApi {
     }
 
     @Override
-    public HostedPaymentPageFormDescriptor buildFormDescriptor(final UUID kbAccountId, final HostedPaymentPageDescriptorFields hostedPaymentPageDescriptorFields, final Iterable<PluginProperty> properties, final TenantContext tenantContext) {
+    public HostedPaymentPageFormDescriptor buildFormDescriptor(final UUID kbAccountId, final Iterable<PluginProperty> customFields, final Iterable<PluginProperty> properties, final CallContext callContext) {
         return null;
     }
 
     @Override
-    public HostedPaymentPageNotification processNotification(final String notification, final Iterable<PluginProperty> properties, final TenantContext tenantContext) throws PaymentPluginApiException {
+    public GatewayNotification processNotification(final String notification, final Iterable<PluginProperty> properties, final CallContext callContext) throws PaymentPluginApiException {
         return null;
     }
 }
diff --git a/payment/src/test/java/org/killbill/billing/payment/dispatcher/TestPluginDispatcher.java b/payment/src/test/java/org/killbill/billing/payment/dispatcher/TestPluginDispatcher.java
index a95ddc3..021707e 100644
--- a/payment/src/test/java/org/killbill/billing/payment/dispatcher/TestPluginDispatcher.java
+++ b/payment/src/test/java/org/killbill/billing/payment/dispatcher/TestPluginDispatcher.java
@@ -36,7 +36,7 @@ public class TestPluginDispatcher extends PaymentTestSuiteNoDB {
     public void testDispatchWithTimeout() throws TimeoutException, PaymentApiException {
         boolean gotIt = false;
         try {
-            voidPluginDispatcher.dispatchWithAccountLockAndTimeout(new Callable<Void>() {
+            voidPluginDispatcher.dispatchWithTimeout(new Callable<Void>() {
                 @Override
                 public Void call() throws Exception {
                     Thread.sleep(1000);
@@ -56,7 +56,7 @@ public class TestPluginDispatcher extends PaymentTestSuiteNoDB {
     public void testDispatchWithPaymentApiException() throws TimeoutException, PaymentApiException {
         boolean gotIt = false;
         try {
-            voidPluginDispatcher.dispatchWithAccountLockAndTimeout(new Callable<Void>() {
+            voidPluginDispatcher.dispatchWithTimeout(new Callable<Void>() {
                 @Override
                 public Void call() throws Exception {
                     throw new PaymentApiException(ErrorCode.PAYMENT_ADD_PAYMENT_METHOD, "foo", "foo");
@@ -75,7 +75,7 @@ public class TestPluginDispatcher extends PaymentTestSuiteNoDB {
     public void testDispatchWithRuntimeExceptionWrappedInPaymentApiException() throws TimeoutException, PaymentApiException {
         boolean gotIt = false;
         try {
-            voidPluginDispatcher.dispatchWithAccountLockAndTimeout(new Callable<Void>() {
+            voidPluginDispatcher.dispatchWithTimeout(new Callable<Void>() {
                 @Override
                 public Void call() throws Exception {
                     throw new RuntimeException("whatever");
diff --git a/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java b/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java
index 665108d..41dad0e 100644
--- a/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java
+++ b/payment/src/test/java/org/killbill/billing/payment/provider/MockPaymentProviderPlugin.java
@@ -30,9 +30,8 @@ import org.killbill.billing.catalog.api.Currency;
 import org.killbill.billing.payment.api.PaymentMethodPlugin;
 import org.killbill.billing.payment.api.PluginProperty;
 import org.killbill.billing.payment.api.TestPaymentMethodPlugin;
-import org.killbill.billing.payment.plugin.api.HostedPaymentPageDescriptorFields;
+import org.killbill.billing.payment.plugin.api.GatewayNotification;
 import org.killbill.billing.payment.plugin.api.HostedPaymentPageFormDescriptor;
-import org.killbill.billing.payment.plugin.api.HostedPaymentPageNotification;
 import org.killbill.billing.payment.plugin.api.NoOpPaymentPluginApi;
 import org.killbill.billing.payment.plugin.api.PaymentInfoPlugin;
 import org.killbill.billing.payment.plugin.api.PaymentMethodInfoPlugin;
@@ -204,12 +203,12 @@ public class MockPaymentProviderPlugin implements NoOpPaymentPluginApi {
     }
 
     @Override
-    public HostedPaymentPageFormDescriptor buildFormDescriptor(final UUID kbAccountId, final HostedPaymentPageDescriptorFields hostedPaymentPageDescriptorFields, final Iterable<PluginProperty> properties, final TenantContext tenantContext) {
+    public HostedPaymentPageFormDescriptor buildFormDescriptor(final UUID kbAccountId, final Iterable<PluginProperty> customFields, final Iterable<PluginProperty> properties, final CallContext callContext) {
         return null;
     }
 
     @Override
-    public HostedPaymentPageNotification processNotification(final String notification, final Iterable<PluginProperty> properties, final TenantContext tenantContext) throws PaymentPluginApiException {
+    public GatewayNotification processNotification(final String notification, final Iterable<PluginProperty> properties, final CallContext callContext) throws PaymentPluginApiException {
         return null;
     }
 

pom.xml 2(+1 -1)

diff --git a/pom.xml b/pom.xml
index 149d4c8..ef91d0f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -19,7 +19,7 @@
     <parent>
         <artifactId>killbill-oss-parent</artifactId>
         <groupId>org.kill-bill.billing</groupId>
-        <version>0.7.7</version>
+        <version>0.7.8</version>
     </parent>
     <artifactId>killbill</artifactId>
     <version>0.11.3-SNAPSHOT</version>