@@ -122,6 +122,7 @@ public class ApiConstants { | |||
public static final String EP_PAYMENT_METHODS = PAYMENT_METHODS_ENDPOINT; | |||
public static final String EP_PAYMENT = "/payment"; | |||
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_BILLS = "/bills"; | |||
public static final String EP_CLOSEST = "/closest"; | |||
@@ -95,6 +95,7 @@ public abstract class PaymentDriverBase<T> extends CloudServiceDriverBase<T> imp | |||
} catch (RuntimeException e) { | |||
// record failed payment, rethrow | |||
accountPaymentDAO.create(new AccountPayment() | |||
.setType(AccountPaymentType.payment) | |||
.setAccount(accountPlan.getAccount()) | |||
.setPlan(accountPlan.getPlan()) | |||
.setAccountPlan(accountPlan.getUuid()) | |||
@@ -212,7 +213,7 @@ public abstract class PaymentDriverBase<T> extends CloudServiceDriverBase<T> imp | |||
String refundInfo) { | |||
// record the payment | |||
final AccountPayment accountPayment = accountPaymentDAO.create(new AccountPayment() | |||
.setType(AccountPaymentType.payment) | |||
.setType(AccountPaymentType.refund) | |||
.setAccount(accountPlan.getAccount()) | |||
.setPlan(accountPlan.getPlan()) | |||
.setAccountPlan(accountPlan.getUuid()) | |||
@@ -151,7 +151,7 @@ public class CodePaymentDriver extends PaymentDriverBase<DefaultPaymentDriverCon | |||
throw invalidEx("err.purchase.tokenInvalid"); | |||
} | |||
if (cpToken.expired()) throw invalidEx("err.purchase.tokenExpired"); | |||
if (!cpToken.hasPaymentMethod(accountPlan.getUuid())) { | |||
if (!cpToken.hasAccountPlan(accountPlan.getUuid())) { | |||
throw invalidEx("err.purchase.tokenInvalid"); | |||
} | |||
return cpToken.getToken(); | |||
@@ -23,7 +23,7 @@ public class CodePaymentToken { | |||
@Getter @Setter private Long 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); | |||
} | |||
@@ -2,6 +2,7 @@ package bubble.dao.bill; | |||
import bubble.dao.account.AccountOwnedEntityDAO; | |||
import bubble.model.bill.AccountPayment; | |||
import org.hibernate.criterion.Order; | |||
import org.springframework.stereotype.Repository; | |||
import java.util.List; | |||
@@ -9,6 +10,9 @@ import java.util.List; | |||
@Repository | |||
public class AccountPaymentDAO extends AccountOwnedEntityDAO<AccountPayment> { | |||
// newest first | |||
@Override public Order getDefaultSortOrder() { return Order.desc("ctime"); } | |||
public List<AccountPayment> findByAccountAndPlan(String accountUuid, String accountPlanUuid) { | |||
return findByFields("account", accountUuid, "plan", accountPlanUuid); | |||
} | |||
@@ -8,6 +8,7 @@ import bubble.model.bill.*; | |||
import bubble.model.cloud.BubbleNetwork; | |||
import bubble.model.cloud.BubbleNetworkState; | |||
import bubble.model.cloud.CloudService; | |||
import bubble.notify.payment.PaymentValidationResult; | |||
import bubble.server.BubbleConfiguration; | |||
import bubble.service.bill.RefundService; | |||
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.getPaymentMethodObject().hasUuid()) throw invalidEx("err.paymentMethod.required"); | |||
if (accountPlan.getPaymentMethod() == null) { | |||
accountPlan.setPaymentMethod(accountPlan.getPaymentMethodObject().getUuid()); | |||
} | |||
final CloudService paymentService = cloudDAO.findByUuid(accountPlan.getPaymentMethodObject().getCloud()); | |||
if (paymentService == null) throw invalidEx("err.paymentService.notFound"); | |||
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()) { | |||
final BubblePlan plan = planDAO.findByUuid(accountPlan.getPlan()); | |||
paymentDriver.authorize(plan, accountPlan.getPaymentMethodObject()); | |||
@@ -1,24 +1,37 @@ | |||
package bubble.resources.bill; | |||
import bubble.cloud.payment.PaymentServiceDriver; | |||
import bubble.dao.bill.AccountPaymentMethodDAO; | |||
import bubble.dao.bill.BillDAO; | |||
import bubble.dao.cloud.CloudServiceDAO; | |||
import bubble.model.account.Account; | |||
import bubble.model.bill.AccountPaymentMethod; | |||
import bubble.model.bill.AccountPlan; | |||
import bubble.model.bill.Bill; | |||
import bubble.model.cloud.CloudService; | |||
import bubble.resources.account.ReadOnlyAccountOwnedResource; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.cobbzilla.wizard.validation.ValidationResult; | |||
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.PathParam; | |||
import javax.ws.rs.core.Context; | |||
import javax.ws.rs.core.Response; | |||
import java.util.List; | |||
import static bubble.ApiConstants.EP_PAY; | |||
import static bubble.ApiConstants.EP_PAYMENTS; | |||
import static org.cobbzilla.wizard.resources.ResourceUtil.notFoundEx; | |||
import static org.cobbzilla.wizard.resources.ResourceUtil.*; | |||
@Slf4j | |||
public class BillsResource extends ReadOnlyAccountOwnedResource<Bill, BillDAO> { | |||
@Autowired private AccountPaymentMethodDAO paymentMethodDAO; | |||
@Autowired private CloudServiceDAO cloudDAO; | |||
private AccountPlan accountPlan; | |||
public BillsResource(Account account) { super(account); } | |||
@@ -46,4 +59,34 @@ public class BillsResource extends ReadOnlyAccountOwnedResource<Bill, BillDAO> { | |||
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.backupCleaner.didNotRun=Backup cleaner did not run | |||
err.backupCleaner.neverRun=Backup cleaner was never run | |||
err.bill.alreadyPaid=Bill has already been paid | |||
err.cannotCreate=Create not allowed | |||
err.cannotUpdate=Update not allowed | |||
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.stop.error=Error stopping node | |||
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.required=Payment information is required | |||
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.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.paymentMethodType.required=Payment method type is required | |||
err.paymentMethodType.mismatch=Payment method type mismatch | |||
@@ -89,6 +89,7 @@ | |||
}, | |||
{ | |||
"before": "sleep 15s", | |||
"comment": "verify account payment methods, should be one", | |||
"request": { "uri": "me/paymentMethods" }, | |||
"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", | |||
"plan": "{{plans.[0].name}}", | |||
"footprint": "US", | |||
"paymentMethod": { | |||
"paymentMethodObject": { | |||
"paymentMethodType": "credit", | |||
"paymentInfo": "invalid_token" | |||
} | |||
@@ -86,7 +86,7 @@ | |||
"timezone": "EST", | |||
"plan": "{{plans.[0].name}}", | |||
"footprint": "US", | |||
"paymentMethod": { | |||
"paymentMethodObject": { | |||
"paymentMethodType": "credit", | |||
"paymentInfo": "{{stripeToken}}" | |||
} | |||
@@ -138,7 +138,7 @@ | |||
"timezone": "EST", | |||
"plan": "{{plans.[0].name}}", | |||
"footprint": "US", | |||
"paymentMethod": { | |||
"paymentMethodObject": { | |||
"paymentMethodType": "credit", | |||
"paymentInfo": "{{stripeToken}}" | |||
} | |||