@@ -122,6 +122,7 @@ public class ApiConstants { | |||||
public static final String EP_PAYMENT_METHODS = PAYMENT_METHODS_ENDPOINT; | public static final String EP_PAYMENT_METHODS = PAYMENT_METHODS_ENDPOINT; | ||||
public static final String EP_PAYMENT = "/payment"; | public static final String EP_PAYMENT = "/payment"; | ||||
public static final String EP_PAYMENTS = "/payments"; | public static final String EP_PAYMENTS = "/payments"; | ||||
public static final String EP_PAY = "/pay"; | |||||
public static final String EP_BILL = "/bill"; | public static final String EP_BILL = "/bill"; | ||||
public static final String EP_BILLS = "/bills"; | public static final String EP_BILLS = "/bills"; | ||||
public static final String EP_CLOSEST = "/closest"; | public static final String EP_CLOSEST = "/closest"; | ||||
@@ -95,6 +95,7 @@ public abstract class PaymentDriverBase<T> extends CloudServiceDriverBase<T> imp | |||||
} catch (RuntimeException e) { | } catch (RuntimeException e) { | ||||
// record failed payment, rethrow | // record failed payment, rethrow | ||||
accountPaymentDAO.create(new AccountPayment() | accountPaymentDAO.create(new AccountPayment() | ||||
.setType(AccountPaymentType.payment) | |||||
.setAccount(accountPlan.getAccount()) | .setAccount(accountPlan.getAccount()) | ||||
.setPlan(accountPlan.getPlan()) | .setPlan(accountPlan.getPlan()) | ||||
.setAccountPlan(accountPlan.getUuid()) | .setAccountPlan(accountPlan.getUuid()) | ||||
@@ -212,7 +213,7 @@ public abstract class PaymentDriverBase<T> extends CloudServiceDriverBase<T> imp | |||||
String refundInfo) { | String refundInfo) { | ||||
// record the payment | // record the payment | ||||
final AccountPayment accountPayment = accountPaymentDAO.create(new AccountPayment() | final AccountPayment accountPayment = accountPaymentDAO.create(new AccountPayment() | ||||
.setType(AccountPaymentType.payment) | |||||
.setType(AccountPaymentType.refund) | |||||
.setAccount(accountPlan.getAccount()) | .setAccount(accountPlan.getAccount()) | ||||
.setPlan(accountPlan.getPlan()) | .setPlan(accountPlan.getPlan()) | ||||
.setAccountPlan(accountPlan.getUuid()) | .setAccountPlan(accountPlan.getUuid()) | ||||
@@ -151,7 +151,7 @@ public class CodePaymentDriver extends PaymentDriverBase<DefaultPaymentDriverCon | |||||
throw invalidEx("err.purchase.tokenInvalid"); | throw invalidEx("err.purchase.tokenInvalid"); | ||||
} | } | ||||
if (cpToken.expired()) throw invalidEx("err.purchase.tokenExpired"); | if (cpToken.expired()) throw invalidEx("err.purchase.tokenExpired"); | ||||
if (!cpToken.hasPaymentMethod(accountPlan.getUuid())) { | |||||
if (!cpToken.hasAccountPlan(accountPlan.getUuid())) { | |||||
throw invalidEx("err.purchase.tokenInvalid"); | throw invalidEx("err.purchase.tokenInvalid"); | ||||
} | } | ||||
return cpToken.getToken(); | return cpToken.getToken(); | ||||
@@ -23,7 +23,7 @@ public class CodePaymentToken { | |||||
@Getter @Setter private Long expiration; | @Getter @Setter private Long expiration; | ||||
public boolean expired() { return expiration != null && now() > expiration; } | public boolean expired() { return expiration != null && now() > expiration; } | ||||
public boolean hasPaymentMethod(String accountPlan) { | |||||
public boolean hasAccountPlan(String accountPlan) { | |||||
return this.accountPlan != null && this.accountPlan.equals(accountPlan); | return this.accountPlan != null && this.accountPlan.equals(accountPlan); | ||||
} | } | ||||
@@ -2,6 +2,7 @@ package bubble.dao.bill; | |||||
import bubble.dao.account.AccountOwnedEntityDAO; | import bubble.dao.account.AccountOwnedEntityDAO; | ||||
import bubble.model.bill.AccountPayment; | import bubble.model.bill.AccountPayment; | ||||
import org.hibernate.criterion.Order; | |||||
import org.springframework.stereotype.Repository; | import org.springframework.stereotype.Repository; | ||||
import java.util.List; | import java.util.List; | ||||
@@ -9,6 +10,9 @@ import java.util.List; | |||||
@Repository | @Repository | ||||
public class AccountPaymentDAO extends AccountOwnedEntityDAO<AccountPayment> { | public class AccountPaymentDAO extends AccountOwnedEntityDAO<AccountPayment> { | ||||
// newest first | |||||
@Override public Order getDefaultSortOrder() { return Order.desc("ctime"); } | |||||
public List<AccountPayment> findByAccountAndPlan(String accountUuid, String accountPlanUuid) { | public List<AccountPayment> findByAccountAndPlan(String accountUuid, String accountPlanUuid) { | ||||
return findByFields("account", accountUuid, "plan", accountPlanUuid); | return findByFields("account", accountUuid, "plan", accountPlanUuid); | ||||
} | } | ||||
@@ -8,6 +8,7 @@ import bubble.model.bill.*; | |||||
import bubble.model.cloud.BubbleNetwork; | import bubble.model.cloud.BubbleNetwork; | ||||
import bubble.model.cloud.BubbleNetworkState; | import bubble.model.cloud.BubbleNetworkState; | ||||
import bubble.model.cloud.CloudService; | import bubble.model.cloud.CloudService; | ||||
import bubble.notify.payment.PaymentValidationResult; | |||||
import bubble.server.BubbleConfiguration; | import bubble.server.BubbleConfiguration; | ||||
import bubble.service.bill.RefundService; | import bubble.service.bill.RefundService; | ||||
import org.springframework.beans.factory.annotation.Autowired; | import org.springframework.beans.factory.annotation.Autowired; | ||||
@@ -47,10 +48,18 @@ public class AccountPlanDAO extends AccountOwnedEntityDAO<AccountPlan> { | |||||
if (!accountPlan.hasPaymentMethodObject()) throw invalidEx("err.paymentMethod.required"); | if (!accountPlan.hasPaymentMethodObject()) throw invalidEx("err.paymentMethod.required"); | ||||
if (!accountPlan.getPaymentMethodObject().hasUuid()) throw invalidEx("err.paymentMethod.required"); | if (!accountPlan.getPaymentMethodObject().hasUuid()) throw invalidEx("err.paymentMethod.required"); | ||||
if (accountPlan.getPaymentMethod() == null) { | |||||
accountPlan.setPaymentMethod(accountPlan.getPaymentMethodObject().getUuid()); | |||||
} | |||||
final CloudService paymentService = cloudDAO.findByUuid(accountPlan.getPaymentMethodObject().getCloud()); | final CloudService paymentService = cloudDAO.findByUuid(accountPlan.getPaymentMethodObject().getCloud()); | ||||
if (paymentService == null) throw invalidEx("err.paymentService.notFound"); | if (paymentService == null) throw invalidEx("err.paymentService.notFound"); | ||||
final PaymentServiceDriver paymentDriver = paymentService.getPaymentDriver(configuration); | final PaymentServiceDriver paymentDriver = paymentService.getPaymentDriver(configuration); | ||||
if (paymentDriver.getPaymentMethodType().requiresClaim()) { | |||||
final PaymentValidationResult result = paymentDriver.claim(accountPlan); | |||||
if (result.hasErrors()) throw invalidEx(result.violationsList()); | |||||
} | |||||
if (paymentDriver.getPaymentMethodType().requiresAuth()) { | if (paymentDriver.getPaymentMethodType().requiresAuth()) { | ||||
final BubblePlan plan = planDAO.findByUuid(accountPlan.getPlan()); | final BubblePlan plan = planDAO.findByUuid(accountPlan.getPlan()); | ||||
paymentDriver.authorize(plan, accountPlan.getPaymentMethodObject()); | paymentDriver.authorize(plan, accountPlan.getPaymentMethodObject()); | ||||
@@ -1,24 +1,37 @@ | |||||
package bubble.resources.bill; | package bubble.resources.bill; | ||||
import bubble.cloud.payment.PaymentServiceDriver; | |||||
import bubble.dao.bill.AccountPaymentMethodDAO; | |||||
import bubble.dao.bill.BillDAO; | import bubble.dao.bill.BillDAO; | ||||
import bubble.dao.cloud.CloudServiceDAO; | |||||
import bubble.model.account.Account; | import bubble.model.account.Account; | ||||
import bubble.model.bill.AccountPaymentMethod; | |||||
import bubble.model.bill.AccountPlan; | import bubble.model.bill.AccountPlan; | ||||
import bubble.model.bill.Bill; | import bubble.model.bill.Bill; | ||||
import bubble.model.cloud.CloudService; | |||||
import bubble.resources.account.ReadOnlyAccountOwnedResource; | import bubble.resources.account.ReadOnlyAccountOwnedResource; | ||||
import lombok.extern.slf4j.Slf4j; | import lombok.extern.slf4j.Slf4j; | ||||
import org.cobbzilla.wizard.validation.ValidationResult; | |||||
import org.glassfish.jersey.server.ContainerRequest; | import org.glassfish.jersey.server.ContainerRequest; | ||||
import org.springframework.beans.factory.annotation.Autowired; | |||||
import javax.ws.rs.POST; | |||||
import javax.ws.rs.Path; | import javax.ws.rs.Path; | ||||
import javax.ws.rs.PathParam; | import javax.ws.rs.PathParam; | ||||
import javax.ws.rs.core.Context; | import javax.ws.rs.core.Context; | ||||
import javax.ws.rs.core.Response; | |||||
import java.util.List; | import java.util.List; | ||||
import static bubble.ApiConstants.EP_PAY; | |||||
import static bubble.ApiConstants.EP_PAYMENTS; | import static bubble.ApiConstants.EP_PAYMENTS; | ||||
import static org.cobbzilla.wizard.resources.ResourceUtil.notFoundEx; | |||||
import static org.cobbzilla.wizard.resources.ResourceUtil.*; | |||||
@Slf4j | @Slf4j | ||||
public class BillsResource extends ReadOnlyAccountOwnedResource<Bill, BillDAO> { | public class BillsResource extends ReadOnlyAccountOwnedResource<Bill, BillDAO> { | ||||
@Autowired private AccountPaymentMethodDAO paymentMethodDAO; | |||||
@Autowired private CloudServiceDAO cloudDAO; | |||||
private AccountPlan accountPlan; | private AccountPlan accountPlan; | ||||
public BillsResource(Account account) { super(account); } | public BillsResource(Account account) { super(account); } | ||||
@@ -46,4 +59,34 @@ public class BillsResource extends ReadOnlyAccountOwnedResource<Bill, BillDAO> { | |||||
return configuration.subResource(AccountPaymentsResource.class, account, bill); | return configuration.subResource(AccountPaymentsResource.class, account, bill); | ||||
} | } | ||||
@POST @Path("/{id}"+EP_PAY) | |||||
public Response payBill(@Context ContainerRequest ctx, | |||||
@PathParam("id") String id, | |||||
AccountPaymentMethod paymentMethod) { | |||||
final Bill bill = super.find(ctx, id); | |||||
if (bill == null) return notFound(id); | |||||
if (bill.paid()) return invalid("err.bill.alreadyPaid"); | |||||
final AccountPaymentMethod payMethodToUse; | |||||
if (paymentMethod.hasUuid()) { | |||||
payMethodToUse = paymentMethodDAO.findByUuid(paymentMethod.getUuid()); | |||||
if (payMethodToUse == null) return invalid("err.paymentMethod.notFound"); | |||||
} else { | |||||
final ValidationResult result = new ValidationResult(); | |||||
paymentMethod.setAccount(getAccountUuid(ctx)).validate(result, configuration); | |||||
if (result.isInvalid()) return invalid(result); | |||||
payMethodToUse = paymentMethodDAO.create(paymentMethod); | |||||
} | |||||
final CloudService paymentCloud = cloudDAO.findByUuid(payMethodToUse.getCloud()); | |||||
if (paymentCloud == null) return invalid("err.paymentService.notFound"); | |||||
final PaymentServiceDriver paymentDriver = paymentCloud.getPaymentDriver(configuration); | |||||
if (paymentDriver.purchase(bill.getAccountPlan(), payMethodToUse.getUuid(), bill.getUuid())) { | |||||
// re-lookup bill, should now be paid | |||||
return ok(getDao().findByUuid(bill.getUuid())); | |||||
} else { | |||||
return invalid("err.purchase.declined"); | |||||
} | |||||
} | |||||
} | } |
@@ -82,6 +82,7 @@ err.authenticator.notConfigured=Authenticator has not been configured | |||||
err.backup.cannotDelete=Cannot delete backup with its current status | err.backup.cannotDelete=Cannot delete backup with its current status | ||||
err.backupCleaner.didNotRun=Backup cleaner did not run | err.backupCleaner.didNotRun=Backup cleaner did not run | ||||
err.backupCleaner.neverRun=Backup cleaner was never run | err.backupCleaner.neverRun=Backup cleaner was never run | ||||
err.bill.alreadyPaid=Bill has already been paid | |||||
err.cannotCreate=Create not allowed | err.cannotCreate=Create not allowed | ||||
err.cannotUpdate=Update not allowed | err.cannotUpdate=Update not allowed | ||||
err.chargeName.required=Charge name is required | err.chargeName.required=Charge name is required | ||||
@@ -159,13 +160,14 @@ err.node.running=Node must be stopped before deleting | |||||
err.node.shutdownFailed=Node shutdown failed | err.node.shutdownFailed=Node shutdown failed | ||||
err.node.stop.error=Error stopping node | err.node.stop.error=Error stopping node | ||||
err.oldPassword.invalid=Old password was invalid | err.oldPassword.invalid=Old password was invalid | ||||
err.paymentMethod.required=Payment method is required | |||||
err.paymentMethod.cannotDeleteInUse=Cannot delete payment method that is currently in use | |||||
err.paymentInfo.invalid=Payment information is invalid | err.paymentInfo.invalid=Payment information is invalid | ||||
err.paymentInfo.required=Payment information is required | err.paymentInfo.required=Payment information is required | ||||
err.paymentInfo.processingError=Processing payment information failed | err.paymentInfo.processingError=Processing payment information failed | ||||
err.paymentInfo.emailRequired=To use this payment method, please add an email address to your account | err.paymentInfo.emailRequired=To use this payment method, please add an email address to your account | ||||
err.paymentInfo.verifiedEmailRequired=To use this payment method, please verify your email address | err.paymentInfo.verifiedEmailRequired=To use this payment method, please verify your email address | ||||
err.paymentMethod.required=Payment method is required | |||||
err.paymentMethod.notFound=Payment method not found | |||||
err.paymentMethod.cannotDeleteInUse=Cannot delete payment method that is currently in use | |||||
err.paymentMethodInfo.invalid=Payment method information is invalid | err.paymentMethodInfo.invalid=Payment method information is invalid | ||||
err.paymentMethodType.required=Payment method type is required | err.paymentMethodType.required=Payment method type is required | ||||
err.paymentMethodType.mismatch=Payment method type mismatch | err.paymentMethodType.mismatch=Payment method type mismatch | ||||
@@ -89,6 +89,7 @@ | |||||
}, | }, | ||||
{ | { | ||||
"before": "sleep 15s", | |||||
"comment": "verify account payment methods, should be one", | "comment": "verify account payment methods, should be one", | ||||
"request": { "uri": "me/paymentMethods" }, | "request": { "uri": "me/paymentMethods" }, | ||||
"response": { | "response": { | ||||
@@ -148,18 +149,28 @@ | |||||
}, | }, | ||||
{ | { | ||||
"comment": "check payments for plan, expect one failed payment" | |||||
}, | |||||
{ | |||||
"comment": "pay for plan with a different code, succeeds" | |||||
}, | |||||
{ | |||||
"comment": "check payments for plan, expect two payments, one failed, the second successful" | |||||
}, | |||||
{ | |||||
"comment": "try to add plan with expired code, expect error" | |||||
"comment": "try to add plan with expired code, expect error", | |||||
"request": { | |||||
"uri": "me/plans", | |||||
"method": "put", | |||||
"entity": { | |||||
"name": "test-net3-{{rand 5}}", | |||||
"domain": "{{defaultDomain}}", | |||||
"locale": "en_US", | |||||
"timezone": "EST", | |||||
"plan": "{{plans.[0].name}}", | |||||
"footprint": "US", | |||||
"paymentMethodObject": { | |||||
"paymentMethodType": "code", | |||||
"paymentInfo": "expired_invite_token" | |||||
} | |||||
} | |||||
}, | |||||
"response": { | |||||
"status": 422, | |||||
"check": [ | |||||
{"condition": "json.has('err.purchase.tokenExpired')"} | |||||
] | |||||
} | |||||
} | } | ||||
] | ] |
@@ -60,7 +60,7 @@ | |||||
"timezone": "EST", | "timezone": "EST", | ||||
"plan": "{{plans.[0].name}}", | "plan": "{{plans.[0].name}}", | ||||
"footprint": "US", | "footprint": "US", | ||||
"paymentMethod": { | |||||
"paymentMethodObject": { | |||||
"paymentMethodType": "credit", | "paymentMethodType": "credit", | ||||
"paymentInfo": "invalid_token" | "paymentInfo": "invalid_token" | ||||
} | } | ||||
@@ -86,7 +86,7 @@ | |||||
"timezone": "EST", | "timezone": "EST", | ||||
"plan": "{{plans.[0].name}}", | "plan": "{{plans.[0].name}}", | ||||
"footprint": "US", | "footprint": "US", | ||||
"paymentMethod": { | |||||
"paymentMethodObject": { | |||||
"paymentMethodType": "credit", | "paymentMethodType": "credit", | ||||
"paymentInfo": "{{stripeToken}}" | "paymentInfo": "{{stripeToken}}" | ||||
} | } | ||||
@@ -138,7 +138,7 @@ | |||||
"timezone": "EST", | "timezone": "EST", | ||||
"plan": "{{plans.[0].name}}", | "plan": "{{plans.[0].name}}", | ||||
"footprint": "US", | "footprint": "US", | ||||
"paymentMethod": { | |||||
"paymentMethodObject": { | |||||
"paymentMethodType": "credit", | "paymentMethodType": "credit", | ||||
"paymentInfo": "{{stripeToken}}" | "paymentInfo": "{{stripeToken}}" | ||||
} | } | ||||