瀏覽代碼

WIP. developing authorize step prior to capture.

tags/v0.1.6
Jonathan Cobb 5 年之前
父節點
當前提交
0485123632
共有 49 個檔案被更改,包括 1321 行新增327 行删除
  1. +2
    -0
      bubble-server/src/main/java/bubble/ApiConstants.java
  2. +177
    -0
      bubble-server/src/main/java/bubble/cloud/payment/PaymentDriverBase.java
  3. +5
    -7
      bubble-server/src/main/java/bubble/cloud/payment/PaymentServiceDriver.java
  4. +23
    -71
      bubble-server/src/main/java/bubble/cloud/payment/code/CodePaymentDriver.java
  5. +42
    -19
      bubble-server/src/main/java/bubble/cloud/payment/delegate/DelegatedPaymentDriver.java
  6. +8
    -11
      bubble-server/src/main/java/bubble/cloud/payment/free/FreePaymentDriver.java
  7. +118
    -55
      bubble-server/src/main/java/bubble/cloud/payment/stripe/StripePaymentDriver.java
  8. +1
    -1
      bubble-server/src/main/java/bubble/dao/bill/AccountPaymentMethodDAO.java
  9. +48
    -4
      bubble-server/src/main/java/bubble/dao/bill/AccountPlanDAO.java
  10. +43
    -0
      bubble-server/src/main/java/bubble/dao/bill/AccountPlanPaymentDAO.java
  11. +1
    -1
      bubble-server/src/main/java/bubble/dao/bill/AccountPlanPaymentMethodDAO.java
  12. +2
    -2
      bubble-server/src/main/java/bubble/dao/bill/BillDAO.java
  13. +38
    -19
      bubble-server/src/main/java/bubble/model/bill/AccountPayment.java
  14. +1
    -1
      bubble-server/src/main/java/bubble/model/bill/AccountPaymentMethod.java
  15. +4
    -0
      bubble-server/src/main/java/bubble/model/bill/AccountPlan.java
  16. +69
    -0
      bubble-server/src/main/java/bubble/model/bill/AccountPlanPayment.java
  17. +14
    -6
      bubble-server/src/main/java/bubble/model/bill/Bill.java
  18. +10
    -1
      bubble-server/src/main/java/bubble/model/bill/BillPeriod.java
  19. +13
    -0
      bubble-server/src/main/java/bubble/model/bill/BillStatus.java
  20. +1
    -1
      bubble-server/src/main/java/bubble/model/bill/BubblePlan.java
  21. +2
    -1
      bubble-server/src/main/java/bubble/model/bill/PaymentMethodType.java
  22. +5
    -3
      bubble-server/src/main/java/bubble/model/cloud/notify/NotificationType.java
  23. +1
    -0
      bubble-server/src/main/java/bubble/notify/NewNodeNotification.java
  24. +1
    -0
      bubble-server/src/main/java/bubble/notify/NotificationHandler_hello_from_sage.java
  25. +12
    -9
      bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver.java
  26. +26
    -0
      bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_authorize.java
  27. +0
    -1
      bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_claim.java
  28. +1
    -3
      bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_purchase.java
  29. +1
    -2
      bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_refund.java
  30. +0
    -1
      bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_validate.java
  31. +2
    -2
      bubble-server/src/main/java/bubble/notify/payment/PaymentMethodClaimNotification.java
  32. +1
    -1
      bubble-server/src/main/java/bubble/notify/payment/PaymentMethodValidationNotification.java
  33. +3
    -4
      bubble-server/src/main/java/bubble/notify/payment/PaymentNotification.java
  34. +50
    -0
      bubble-server/src/main/java/bubble/notify/payment/PaymentResult.java
  35. +2
    -1
      bubble-server/src/main/java/bubble/notify/payment/PaymentValidationResult.java
  36. +18
    -0
      bubble-server/src/main/java/bubble/resources/account/ReadOnlyAccountOwnedResource.java
  37. +2
    -38
      bubble-server/src/main/java/bubble/resources/bill/AccountPaymentsResource.java
  38. +96
    -0
      bubble-server/src/main/java/bubble/resources/bill/AccountPlanPaymentsResource.java
  39. +2
    -2
      bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java
  40. +6
    -11
      bubble-server/src/main/java/bubble/resources/bill/BillsResource.java
  41. +2
    -18
      bubble-server/src/main/java/bubble/resources/cloud/NodesResource.java
  42. +12
    -1
      bubble-server/src/main/java/bubble/service/cloud/StandardNetworkService.java
  43. +4
    -0
      bubble-server/src/main/resources/message_templates/server/en_US/post_auth/ResourceMessages.properties
  44. +3
    -10
      bubble-server/src/test/java/bubble/test/PaymentTest.java
  45. +12
    -3
      bubble-server/src/test/resources/models/tests/payment/pay_code.json
  46. +16
    -5
      bubble-server/src/test/resources/models/tests/payment/pay_credit.json
  47. +332
    -0
      bubble-server/src/test/resources/models/tests/payment/pay_credit_multi_start_stop.json
  48. +88
    -11
      bubble-server/src/test/resources/models/tests/payment/pay_free.json
  49. +1
    -1
      utils/cobbzilla-utils

+ 2
- 0
bubble-server/src/main/java/bubble/ApiConstants.java 查看文件

@@ -120,7 +120,9 @@ public class ApiConstants {
public static final String EP_VPN = "/vpn";
public static final String EP_PAYMENT_METHOD = "/paymentMethod";
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_BILL = "/bill";
public static final String EP_BILLS = "/bills";
public static final String EP_CLOSEST = "/closest";
public static final String EP_ROLES = ROLES_ENDPOINT;


+ 177
- 0
bubble-server/src/main/java/bubble/cloud/payment/PaymentDriverBase.java 查看文件

@@ -2,9 +2,186 @@ package bubble.cloud.payment;

import bubble.cloud.CloudServiceDriverBase;
import bubble.cloud.CloudServiceType;
import bubble.dao.bill.*;
import bubble.model.bill.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.List;

import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx;

@Slf4j
public abstract class PaymentDriverBase<T> extends CloudServiceDriverBase<T> implements PaymentServiceDriver {

@Override public CloudServiceType getType() { return CloudServiceType.payment; }

@Autowired protected AccountPlanDAO accountPlanDAO;
@Autowired protected BubblePlanDAO planDAO;
@Autowired protected AccountPaymentMethodDAO paymentMethodDAO;
@Autowired protected AccountPlanPaymentMethodDAO planPaymentMethodDAO;
@Autowired protected BillDAO billDAO;
@Autowired protected AccountPaymentDAO accountPaymentDAO;
@Autowired protected AccountPlanPaymentDAO accountPlanPaymentDAO;

public AccountPlan getAccountPlan(String accountPlanUuid) {
final AccountPlan accountPlan = accountPlanDAO.findByUuid(accountPlanUuid);
if (accountPlan == null) throw invalidEx("err.purchase.planNotFound");
return accountPlan;
}

public BubblePlan getBubblePlan(AccountPlan accountPlan) {
final BubblePlan plan = planDAO.findByUuid(accountPlan.getPlan());
if (plan == null) throw invalidEx("err.purchase.planNotFound");
return plan;
}

public AccountPaymentMethod getPaymentMethod(AccountPlan accountPlan, String paymentMethodUuid) {
final AccountPaymentMethod paymentMethod = paymentMethodDAO.findByUuid(paymentMethodUuid);
if (paymentMethod == null) throw invalidEx("err.purchase.paymentMethodNotFound");
if (!paymentMethod.getAccount().equals(accountPlan.getAccount())) throw invalidEx("err.purchase.accountMismatch");
if (paymentMethod.getPaymentMethodType() != getPaymentMethodType()) throw invalidEx("err.purchase.paymentMethodMismatch");
return paymentMethod;
}

public AccountPlanPaymentMethod getPlanPaymentMethod(String accountPlanUuid, String paymentMethodUuid) {
final AccountPlanPaymentMethod planPaymentMethod = planPaymentMethodDAO.findCurrentMethodForPlan(accountPlanUuid);
if (planPaymentMethod == null) throw invalidEx("err.purchase.paymentMethodNotSet");
if (!planPaymentMethod.getPaymentMethod().equals(paymentMethodUuid)) throw invalidEx("err.purchase.paymentMethodMismatch");
return planPaymentMethod;
}

public Bill getBill(String billUuid, long purchaseAmount, String currency, AccountPlan accountPlan) {
final Bill bill = billDAO.findByUuid(billUuid);
if (bill == null) throw invalidEx("err.purchase.billNotFound");
if (!bill.getAccount().equals(accountPlan.getAccount())) throw invalidEx("err.purchase.accountMismatch");
if (bill.getTotal() != purchaseAmount) throw invalidEx("err.purchase.amountMismatch");
if (!bill.getCurrency().equals(currency)) throw invalidEx("err.purchase.currencyMismatch");
return bill;
}

@Override public boolean authorize(BubblePlan plan, AccountPlan accountPlan, AccountPaymentMethod paymentMethod) {
return true;
}

@Override public boolean purchase(String accountPlanUuid, String paymentMethodUuid, String billUuid) {

final AccountPlan accountPlan = getAccountPlan(accountPlanUuid);
final BubblePlan plan = getBubblePlan(accountPlan);
final AccountPaymentMethod paymentMethod = getPaymentMethod(accountPlan, paymentMethodUuid);
final Bill bill = getBill(billUuid, plan.getPrice(), plan.getCurrency(), accountPlan);
final AccountPlanPaymentMethod planPaymentMethod = getPlanPaymentMethod(accountPlanUuid, paymentMethodUuid);

if (!paymentMethod.getAccount().equals(accountPlan.getAccount()) || !paymentMethod.getAccount().equals(bill.getAccount())) {
throw invalidEx("err.purchase.billNotFound");
}

// has this already been paid?
final AccountPlanPayment existing = accountPlanPaymentDAO.findByBill(billUuid);
if (existing != null) {
log.warn("purchase: existing AccountPlanPayment found (returning true): "+existing.getUuid());
return true;
}

final AccountPlanPayment priorPayment = findPriorPayment(plan, paymentMethod, bill);
if (priorPayment != null) {
billDAO.update(bill.setStatus(BillStatus.paid));
accountPlanPaymentDAO.create(new AccountPlanPayment()
.setAccount(accountPlan.getAccount())
.setPlan(accountPlan.getPlan())
.setAccountPlan(accountPlan.getUuid())
.setPayment(priorPayment.getPayment())
.setPaymentMethod(priorPayment.getPaymentMethod())
.setPlanPaymentMethod(planPaymentMethod.getUuid())
.setBill(bill.getUuid())
.setPeriod(bill.getPeriod())
.setAmount(0L)
.setCurrency(bill.getCurrency()));

} else {
try {
charge(plan, accountPlan, paymentMethod, planPaymentMethod, bill);
} catch (RuntimeException e) {
// record failed payment, rethrow
accountPaymentDAO.create(new AccountPayment()
.setAccount(accountPlan.getAccount())
.setPlan(accountPlan.getPlan())
.setAccountPlan(accountPlan.getUuid())
.setPaymentMethod(paymentMethod.getUuid())
.setAmount(bill.getTotal())
.setCurrency(bill.getCurrency())
.setStatus(AccountPaymentStatus.failure)
.setError(e)
.setInfo(paymentMethod.getPaymentInfo()));
throw e;
}
recordPayment(bill, accountPlan, paymentMethod, planPaymentMethod);
}
accountPlanDAO.update(accountPlan.setEnabled(true));
return true;
}

public AccountPlanPayment findPriorPayment(BubblePlan plan, AccountPaymentMethod paymentMethod, Bill bill) {
// is there a previous AccountPlanPayment where:
// - it has a price that is the same or more expensive than the BubblePlan being purchased now
// - it has a price in the same currency as the BubblePlan being purchased now
// - it has the same AccountPaymentMethod
// - it is for the current period
// - the corresponding AccountPlan has been deleted
// if so we can re-use that payment and do not need to charge anything now
final List<AccountPlanPayment> priorSimilarPayments = accountPlanPaymentDAO.findByAccountPaymentMethodAndPeriodAndPriceAndCurrency(
paymentMethod.getUuid(),
bill.getPeriod(),
plan.getPrice(),
plan.getCurrency());
for (AccountPlanPayment app : priorSimilarPayments) {
final AccountPlan ap = accountPlanDAO.findByUuid(app.getAccountPlan());
if (ap != null && ap.deleted()) return app;
}
return null;
}

protected abstract void charge(BubblePlan plan,
AccountPlan accountPlan,
AccountPaymentMethod paymentMethod,
AccountPlanPaymentMethod planPaymentMethod,
Bill bill);

@Override public boolean refund(String accountPlanUuid, String paymentMethodUuid, String billUuid, long refundAmount) {
log.error("refund: not yet supported: accountPlanUuid="+accountPlanUuid+", paymentMethodUuid="+paymentMethodUuid+", billUuid="+billUuid);
return false;
}

public AccountPlanPayment recordPayment(Bill bill,
AccountPlan accountPlan,
AccountPaymentMethod paymentMethod,
AccountPlanPaymentMethod planPaymentMethod) {
// mark the bill as paid
billDAO.update(bill.setStatus(BillStatus.paid));

// create the payment
final AccountPayment payment = accountPaymentDAO.create(new AccountPayment()
.setAccount(accountPlan.getAccount())
.setPlan(accountPlan.getPlan())
.setAccountPlan(accountPlan.getUuid())
.setPaymentMethod(paymentMethod.getUuid())
.setAmount(bill.getTotal())
.setCurrency(bill.getCurrency())
.setStatus(AccountPaymentStatus.success)
.setInfo(paymentMethod.getPaymentInfo()));

// associate the payment to the bill
return accountPlanPaymentDAO.create(new AccountPlanPayment()
.setAccount(accountPlan.getAccount())
.setPlan(accountPlan.getPlan())
.setAccountPlan(accountPlan.getUuid())
.setPayment(payment.getUuid())
.setPaymentMethod(paymentMethod.getUuid())
.setPlanPaymentMethod(planPaymentMethod.getUuid())
.setBill(bill.getUuid())
.setPeriod(bill.getPeriod())
.setAmount(bill.getTotal())
.setCurrency(bill.getCurrency()));
}

}

+ 5
- 7
bubble-server/src/main/java/bubble/cloud/payment/PaymentServiceDriver.java 查看文件

@@ -1,9 +1,8 @@
package bubble.cloud.payment;

import bubble.cloud.CloudServiceType;
import bubble.model.bill.AccountPaymentMethod;
import bubble.model.bill.AccountPlanPaymentMethod;
import bubble.model.bill.PaymentMethodType;
import bubble.model.bill.*;
import bubble.notify.payment.PaymentValidationResult;

import static org.cobbzilla.util.daemon.ZillaRuntime.notSupported;

@@ -18,10 +17,9 @@ public interface PaymentServiceDriver {
default PaymentValidationResult claim(AccountPaymentMethod paymentMethod) { return notSupported("claim"); }
default PaymentValidationResult claim(AccountPlanPaymentMethod planPaymentMethod) { return notSupported("claim"); }

boolean purchase(String accountPlanUuid, String paymentMethodUuid, String billUuid,
int purchaseAmount, String currency);
boolean authorize(BubblePlan plan, AccountPlan accountPlan, AccountPaymentMethod paymentMethod);

boolean refund(String accountPlanUuid, String paymentMethodUuid, String billUuid,
int refundAmount, String currency);
boolean purchase(String accountPlanUuid, String paymentMethodUuid, String billUuid);

boolean refund(String accountPlanUuid, String paymentMethodUuid, String billUuid, long refundAmount);
}

+ 23
- 71
bubble-server/src/main/java/bubble/cloud/payment/code/CodePaymentDriver.java 查看文件

@@ -2,7 +2,7 @@ package bubble.cloud.payment.code;

import bubble.cloud.payment.DefaultPaymentDriverConfig;
import bubble.cloud.payment.PaymentDriverBase;
import bubble.cloud.payment.PaymentValidationResult;
import bubble.notify.payment.PaymentValidationResult;
import bubble.dao.bill.*;
import bubble.dao.cloud.CloudServiceDAO;
import bubble.dao.cloud.CloudServiceDataDAO;
@@ -21,12 +21,7 @@ import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx;
@Slf4j
public class CodePaymentDriver extends PaymentDriverBase<DefaultPaymentDriverConfig> {

@Autowired private AccountPlanDAO accountPlanDAO;
@Autowired private AccountPaymentMethodDAO paymentMethodDAO;
@Autowired private AccountPlanPaymentMethodDAO planPaymentMethodDAO;
@Autowired private CloudServiceDataDAO dataDAO;
@Autowired private BillDAO billDAO;
@Autowired private AccountPaymentDAO accountPaymentDAO;

@Override public PaymentMethodType getPaymentMethodType() { return PaymentMethodType.code; }

@@ -96,71 +91,6 @@ public class CodePaymentDriver extends PaymentDriverBase<DefaultPaymentDriverCon
return "X".repeat(12) + info.substring(maskLength);
}

@Override public boolean purchase(String accountPlanUuid, String paymentMethodUuid, String billUuid,
int purchaseAmount, String currency) {
// is the account plan valid?
final AccountPlan accountPlan = accountPlanDAO.findByUuid(accountPlanUuid);
if (accountPlan == null) throw invalidEx("err.purchase.planNotFound");

// is the payment method valid?
final AccountPaymentMethod paymentMethod = paymentMethodDAO.findByUuid(paymentMethodUuid);
if (paymentMethod == null) throw invalidEx("err.purchase.paymentMethodNotFound");
if (!paymentMethod.getAccount().equals(accountPlan.getAccount())) throw invalidEx("err.purchase.accountMismatch");
if (paymentMethod.getPaymentMethodType() != getPaymentMethodType()) throw invalidEx("err.purchase.paymentMethodMismatch");

// is the plan payment method correct?
final AccountPlanPaymentMethod planPaymentMethod = planPaymentMethodDAO.findCurrentMethodForPlan(accountPlanUuid);
if (planPaymentMethod == null) throw invalidEx("err.purchase.paymentMethodNotSet");
if (!planPaymentMethod.getPaymentMethod().equals(paymentMethodUuid)) throw invalidEx("err.purchase.paymentMethodMismatch");

// is the bill valid?
final Bill bill = billDAO.findByUuid(billUuid);
if (bill == null) throw invalidEx("err.purchase.billNotFound");
if (!bill.getAccount().equals(accountPlan.getAccount())) throw invalidEx("err.purchase.accountMismatch");
if (bill.getTotal() != purchaseAmount) throw invalidEx("err.purchase.amountMismatch");
if (!bill.getCurrency().equals(currency)) throw invalidEx("err.purchase.currencyMismatch");

// is the token valid?
final CloudServiceData csData = dataDAO.findByCloudAndKey(cloud.getUuid(), paymentMethod.getPaymentInfo());
if (csData == null) throw invalidEx("err.purchase.tokenNotFound");

final CodePaymentToken cpToken;
try {
cpToken = json(csData.getData(), CodePaymentToken.class);
} catch (Exception e) {
throw invalidEx("err.purchase.tokenInvalid");
}
if (cpToken.expired()) throw invalidEx("err.purchase.tokenExpired");
if (!cpToken.hasPaymentMethod(planPaymentMethod.getUuid())) {
throw invalidEx("err.purchase.tokenInvalid");
}

// // create the bill
// final Bill bill = billDAO.create(new Bill()
// .setAccount(accountPlan.getAccount())
// .setPlan(accountPlan.getUuid())
// .setType(BillItemType.compute)
// .setQuantity(1L)
// .setPrice(purchaseAmount)
// .setCurrency(currency));

// pay the bill
accountPaymentDAO.create(new AccountPayment()
.setAccount(accountPlan.getAccount())
.setPlan(accountPlan.getUuid())
.setPaymentMethod(planPaymentMethod.getUuid())
.setBill(bill.getUuid())
.setInfo(paymentMethod.getPaymentInfo()));

return true;
}

@Override public boolean refund(String accountPlanUuid, String paymentMethodUuid, String billUuid,
int refundAmount, String currency) {
// no refunds, since there are no charges made
return false;
}

public static CodePaymentToken readToken(BubbleConfiguration configuration,
AccountPaymentMethod accountPaymentMethod,
ValidationResult result,
@@ -206,4 +136,26 @@ public class CodePaymentDriver extends PaymentDriverBase<DefaultPaymentDriverCon
}
return null;
}

@Override protected void charge(BubblePlan plan,
AccountPlan accountPlan,
AccountPaymentMethod paymentMethod,
AccountPlanPaymentMethod planPaymentMethod,
Bill bill) {
// is the token valid?
final CloudServiceData csData = dataDAO.findByCloudAndKey(cloud.getUuid(), paymentMethod.getPaymentInfo());
if (csData == null) throw invalidEx("err.purchase.tokenNotFound");

final CodePaymentToken cpToken;
try {
cpToken = json(csData.getData(), CodePaymentToken.class);
} catch (Exception e) {
throw invalidEx("err.purchase.tokenInvalid");
}
if (cpToken.expired()) throw invalidEx("err.purchase.tokenExpired");
if (!cpToken.hasPaymentMethod(planPaymentMethod.getUuid())) {
throw invalidEx("err.purchase.tokenInvalid");
}
}

}

+ 42
- 19
bubble-server/src/main/java/bubble/cloud/payment/delegate/DelegatedPaymentDriver.java 查看文件

@@ -3,20 +3,16 @@ package bubble.cloud.payment.delegate;
import bubble.cloud.CloudServiceType;
import bubble.cloud.DelegatedCloudServiceDriverBase;
import bubble.cloud.payment.PaymentServiceDriver;
import bubble.cloud.payment.PaymentValidationResult;
import bubble.notify.payment.*;
import bubble.dao.cloud.CloudServiceDAO;
import bubble.model.bill.AccountPaymentMethod;
import bubble.model.bill.AccountPlanPaymentMethod;
import bubble.model.bill.PaymentMethodType;
import bubble.model.bill.*;
import bubble.model.cloud.BubbleNode;
import bubble.model.cloud.CloudService;
import bubble.notify.payment.PaymentMethodClaimNotification;
import bubble.notify.payment.PaymentMethodValidationNotification;
import bubble.notify.payment.PaymentNotification;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;

import static bubble.model.cloud.notify.NotificationType.*;
import static org.cobbzilla.util.daemon.ZillaRuntime.die;
import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx;

@Slf4j
@@ -41,39 +37,66 @@ public class DelegatedPaymentDriver extends DelegatedCloudServiceDriverBase impl
@Override public PaymentValidationResult validate(AccountPaymentMethod paymentMethod) {
final BubbleNode delegate = getDelegateNode();
return notificationService.notifySync(delegate, payment_driver_validate,
new PaymentMethodValidationNotification(paymentMethod, cloud.getName()));
new PaymentMethodValidationNotification(cloud.getName(), paymentMethod));
}

@Override public PaymentValidationResult claim(AccountPaymentMethod paymentMethod) {
final BubbleNode delegate = getDelegateNode();
return notificationService.notifySync(delegate, payment_driver_claim,
new PaymentMethodClaimNotification(paymentMethod, cloud.getName()));
new PaymentMethodClaimNotification(cloud.getName(), paymentMethod));
}

@Override public PaymentValidationResult claim(AccountPlanPaymentMethod planPaymentMethod) {
final BubbleNode delegate = getDelegateNode();
return notificationService.notifySync(delegate, payment_driver_claim,
new PaymentMethodClaimNotification(planPaymentMethod, cloud.getName()));
new PaymentMethodClaimNotification(cloud.getName(), planPaymentMethod));
}

@Override public boolean authorize(BubblePlan plan, AccountPlan accountPlan, AccountPaymentMethod paymentMethod) {
final BubbleNode delegate = getDelegateNode();
final PaymentResult result = notificationService.notifySync(delegate, payment_driver_authorize,
new PaymentNotification()
.setCloud(cloud.getName())
.setAccountPlanUuid(accountPlan.getUuid())
.setPaymentMethodUuid(paymentMethod.getUuid()));
return processResult(result);
}

@Override public boolean purchase(String accountPlanUuid,
String paymentMethodUuid,
String billUuid,
int purchaseAmount,
String currency) {
String billUuid) {
final BubbleNode delegate = getDelegateNode();
return notificationService.notifySync(delegate, payment_driver_purchase,
new PaymentNotification(accountPlanUuid, paymentMethodUuid, billUuid, purchaseAmount, currency));
final PaymentResult result = notificationService.notifySync(delegate, payment_driver_purchase,
new PaymentNotification()
.setCloud(cloud.getName())
.setAccountPlanUuid(accountPlanUuid)
.setPaymentMethodUuid(paymentMethodUuid)
.setBillUuid(billUuid));
return processResult(result);
}

@Override public boolean refund(String accountPlanUuid,
String paymentMethodUuid,
String billUuid,
int refundAmount,
String currency) {
long refundAmount) {
final BubbleNode delegate = getDelegateNode();
return notificationService.notifySync(delegate, payment_driver_refund,
new PaymentNotification(accountPlanUuid, paymentMethodUuid, billUuid, refundAmount, currency));
final PaymentResult result = notificationService.notifySync(delegate, payment_driver_refund,
new PaymentNotification()
.setCloud(cloud.getName())
.setAccountPlanUuid(accountPlanUuid)
.setPaymentMethodUuid(paymentMethodUuid)
.setBillUuid(billUuid)
.setAmount(refundAmount));
return processResult(result);
}

public boolean processResult(PaymentResult result) {
if (result.success()) return true;
if (result.hasViolations()) {
throw invalidEx(result.violationList());
}
if (result.hasError()) return die("authorize: "+result.getError());
return false;
}

}

+ 8
- 11
bubble-server/src/main/java/bubble/cloud/payment/free/FreePaymentDriver.java 查看文件

@@ -2,9 +2,8 @@ package bubble.cloud.payment.free;

import bubble.cloud.payment.DefaultPaymentDriverConfig;
import bubble.cloud.payment.PaymentDriverBase;
import bubble.cloud.payment.PaymentValidationResult;
import bubble.model.bill.AccountPaymentMethod;
import bubble.model.bill.PaymentMethodType;
import bubble.notify.payment.PaymentValidationResult;
import bubble.model.bill.*;

public class FreePaymentDriver extends PaymentDriverBase<DefaultPaymentDriverConfig> {

@@ -19,14 +18,12 @@ public class FreePaymentDriver extends PaymentDriverBase<DefaultPaymentDriverCon
return new PaymentValidationResult(paymentMethod.setMaskedPaymentInfo(FREE_MASK));
}

@Override public boolean purchase(String accountPlanUuid, String paymentMethodUuid, String billUuid,
int purchaseAmount, String currency) {
return true;
}

@Override public boolean refund(String accountPlanUuid, String paymentMethodUuid, String billUuid,
int refundAmount, String currency) {
return true;
@Override protected void charge(BubblePlan plan,
AccountPlan accountPlan,
AccountPaymentMethod paymentMethod,
AccountPlanPaymentMethod planPaymentMethod,
Bill bill) {
// noop for free payment driver
}

}

+ 118
- 55
bubble-server/src/main/java/bubble/cloud/payment/stripe/StripePaymentDriver.java 查看文件

@@ -1,12 +1,8 @@
package bubble.cloud.payment.stripe;

import bubble.cloud.payment.PaymentDriverBase;
import bubble.cloud.payment.PaymentValidationResult;
import bubble.notify.payment.PaymentValidationResult;
import bubble.dao.account.AccountPolicyDAO;
import bubble.dao.bill.AccountPaymentMethodDAO;
import bubble.dao.bill.AccountPlanDAO;
import bubble.dao.bill.BillDAO;
import bubble.dao.bill.BubblePlanDAO;
import bubble.model.account.AccountPolicy;
import bubble.model.bill.*;
import com.stripe.Stripe;
@@ -26,6 +22,7 @@ import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import static java.lang.Boolean.TRUE;
import static org.cobbzilla.util.daemon.ZillaRuntime.die;
import static org.cobbzilla.util.daemon.ZillaRuntime.empty;
import static org.cobbzilla.util.json.JsonUtil.COMPACT_MAPPER;
@@ -39,17 +36,15 @@ public class StripePaymentDriver extends PaymentDriverBase<StripePaymentDriverCo

private static final String PARAM_SECRET_API_KEY = "secretApiKey";

public static final long AUTH_CACHE_DURATION = TimeUnit.DAYS.toSeconds(7);
public static final long CHARGE_CACHE_DURATION = TimeUnit.HOURS.toSeconds(24);
private static final Object lock = new Object();

@Autowired private AccountPlanDAO accountPlanDAO;
@Autowired private AccountPaymentMethodDAO paymentMethodDAO;
@Autowired private BillDAO billDAO;
@Autowired private BubblePlanDAO planDAO;
@Autowired private AccountPolicyDAO policyDAO;

@Autowired private RedisService redisService;
@Getter(lazy=true) private final RedisService chargeCache = redisService.prefixNamespace(getClass().getSimpleName());
@Getter(lazy=true) private final RedisService authCache = redisService.prefixNamespace(getClass().getSimpleName()+"_auth");
@Getter(lazy=true) private final RedisService chargeCache = redisService.prefixNamespace(getClass().getSimpleName()+"_charge");

private static final AtomicReference<String> setupDone = new AtomicReference<>(null);

@@ -123,94 +118,162 @@ public class StripePaymentDriver extends PaymentDriverBase<StripePaymentDriverCo
return new PaymentValidationResult(accountPaymentMethod);
}

@Override public boolean purchase(String accountPlanUuid, String paymentMethodUuid, String billUuid,
int purchaseAmount, String currency) {
final AccountPlan accountPlan = accountPlanDAO.findByUuid(accountPlanUuid);
if (accountPlan == null) throw invalidEx("err.purchase.planNotFound");

final Bill bill = billDAO.findByUuid(billUuid);
if (bill == null) throw invalidEx("err.purchase.billNotFound");

final AccountPaymentMethod paymentMethod = paymentMethodDAO.findByUuid(paymentMethodUuid);
if (paymentMethod == null) throw invalidEx("err.paymentMethod.required");

if (!paymentMethod.getAccount().equals(accountPlan.getAccount()) || !paymentMethod.getAccount().equals(bill.getAccount())) {
throw invalidEx("err.purchase.billNotFound");
}
public String getAuthCacheKey(String accountPlanUuid, String paymentMethodUuid) {
return accountPlanUuid+":"+paymentMethodUuid;
}

final BubblePlan plan = planDAO.findByUuid(accountPlan.getPlan());
if (plan == null) throw invalidEx("err.purchase.planNotFound");
@Override public boolean authorize(BubblePlan plan,
AccountPlan accountPlan,
AccountPaymentMethod paymentMethod) {
final String accountPlanUuid = accountPlan.getUuid();
final String paymentMethodUuid = paymentMethod.getUuid();

final Charge charge;
final Map<String, Object> chargeParams = new LinkedHashMap<>();;
final RedisService cache = getChargeCache();
final RedisService authCache = getAuthCache();
try {
chargeParams.put("amount", purchaseAmount); // Amount in cents
chargeParams.put("currency", currency.toLowerCase());
chargeParams.put("amount", plan.getPrice()); // Amount in cents
chargeParams.put("currency", plan.getCurrency().toLowerCase());
chargeParams.put("customer", paymentMethod.getPaymentInfo());
chargeParams.put("description", plan.chargeDescription());
chargeParams.put("statement_description", plan.chargeDescription());
chargeParams.put("capture", false);
final String chargeJson = json(chargeParams, COMPACT_MAPPER);
final String cached = cache.get(billUuid);
if (cached != null) {
try {
charge = json(chargeJson, Charge.class);
} catch (Exception e) {
final String msg = "purchase: error parsing cached charge: " + e;
log.error(msg);
throw invalidEx("err.purchase.cardUnknownError", msg);
}
} else {
synchronized (lock) {
charge = Charge.create(chargeParams);
}
final String authCacheKey = getAuthCacheKey(accountPlanUuid, paymentMethodUuid);
final String chargeId = authCache.get(authCacheKey);
if (chargeId != null) {
log.warn("authorize: already authorized: "+authCacheKey);
return true;
}
synchronized (lock) {
charge = Charge.create(chargeParams);
}
if (charge.getStatus() == null) {
final String msg = "purchase: no status returned for Charge, accountPlan=" + accountPlanUuid + " with paymentMethod=" + paymentMethodUuid + " and bill=" + billUuid;
final String msg = "authorize: no status returned for Charge, accountPlan=" + accountPlanUuid + " with paymentMethod=" + paymentMethodUuid;
log.error(msg);
throw invalidEx("err.purchase.cardUnknownError", msg);
} else {
final String msg;
switch (charge.getStatus()) {
case "succeeded":
log.info("purchase: charge successful: chargeId="+charge.getId()+", charge="+chargeJson);
cache.set(billUuid, json(charge), "EX", CHARGE_CACHE_DURATION);
if (charge.getReview() != null) {
log.info("authorize: successful but is under review: charge=" + chargeJson);
} else {
log.info("authorize: successful: charge=" + chargeJson);
}
authCache.set(authCacheKey, charge.getId(), "EX", AUTH_CACHE_DURATION);
return true;

case "pending":
msg = "purchase: status='pending' (expected 'succeeded'), chargeId="+charge.getId()+", accountPlan=" + accountPlanUuid + " with paymentMethod=" + paymentMethodUuid + " and bill=" + billUuid;
msg = "authorize: status='pending' (expected 'succeeded'), accountPlan=" + accountPlanUuid + " with paymentMethod=" + paymentMethodUuid;
log.error(msg);
throw invalidEx("err.purchase.chargePendingError", msg);

default:
msg = "purchase: status='"+charge.getStatus()+"' (expected 'succeeded'), chargeId="+charge.getId()+", accountPlan=" + accountPlanUuid + " with paymentMethod=" + paymentMethodUuid + " and bill=" + billUuid;
msg = "authorize: status='" + charge.getStatus() + "' (expected 'succeeded'), accountPlan=" + accountPlanUuid + " with paymentMethod=" + paymentMethodUuid;
log.error(msg);
throw invalidEx("err.purchase.chargeFailedError", msg);
}
}

} catch (CardException e) {
// The card has been declined
final String msg = "purchase: CardException for accountPlan=" + accountPlanUuid + " with paymentMethod=" + paymentMethodUuid + " and bill=" + billUuid + ": requestId=" + e.getRequestId() + ", code="+e.getCode()+", declineCode="+e.getDeclineCode()+", error=" + e.toString();
final String msg = "authorize: CardException for accountPlan=" + accountPlanUuid + " with paymentMethod=" + paymentMethodUuid + ": requestId=" + e.getRequestId() + ", code="+e.getCode()+", declineCode="+e.getDeclineCode()+", error=" + e.toString();
log.error(msg);
throw invalidEx("err.purchase.cardError", msg);

} catch (StripeException e) {
final String msg = "purchase: "+e.getClass().getSimpleName()+" for accountPlan=" + accountPlanUuid + " with paymentMethod=" + paymentMethodUuid + " and bill=" + billUuid + ": requestId=" + e.getRequestId() + ", code="+e.getCode()+", error=" + e.toString();
final String msg = "authorize: "+e.getClass().getSimpleName()+" for accountPlan=" + accountPlanUuid + " with paymentMethod=" + paymentMethodUuid + ": requestId=" + e.getRequestId() + ", code="+e.getCode()+", error=" + e.toString();
log.error(msg);
throw invalidEx("err.purchase.cardProcessingError", msg);

} catch (Exception e) {
final String msg = "purchase: "+e.getClass().getSimpleName()+" for accountPlan=" + accountPlanUuid + " with paymentMethod=" + paymentMethodUuid + " and bill=" + billUuid + ": error=" + e.toString();
final String msg = "authorize: "+e.getClass().getSimpleName()+" for accountPlan=" + accountPlanUuid + " with paymentMethod=" + paymentMethodUuid + ": error=" + e.toString();
log.error(msg);
throw invalidEx("err.purchase.cardUnknownError", msg);
}
}

@Override public boolean refund(String accountPlanUuid, String paymentMethodUuid, String billUuid,
int refundAmount, String currency) {
log.error("refund: not yet supported: accountPlanUuid="+accountPlanUuid+", paymentMethodUuid="+paymentMethodUuid+", billUuid="+billUuid);
return false;
@Override protected void charge(BubblePlan plan,
AccountPlan accountPlan,
AccountPaymentMethod paymentMethod,
AccountPlanPaymentMethod planPaymentMethod,
Bill bill) {
final String accountPlanUuid = accountPlan.getUuid();
final String paymentMethodUuid = paymentMethod.getUuid();
final String billUuid = bill.getUuid();

final Charge charge;
final RedisService authCache = getAuthCache();
final RedisService chargeCache = getChargeCache();

final String authCacheKey = getAuthCacheKey(accountPlanUuid, paymentMethodUuid);
try {
final String charged = chargeCache.get(billUuid);
if (charged != null) {
// already charged, nothing to do
log.info("charge: already charged: "+charged);
return;
}

final String chargeId = authCache.get(authCacheKey);
if (chargeId == null) throw invalidEx("err.purchase.authNotFound");

try {
charge = Charge.retrieve(chargeId);
} catch (Exception e) {
final String msg = "charge: error retrieving charge: " + e;
log.error(msg);
throw invalidEx("err.purchase.cardUnknownError", msg);
}
if (charge.getReview() != null) {
final String msg = "charge: charge "+chargeId+" still under review: " + charge.getReview();
log.error(msg);
throw invalidEx("err.purchase.underReview", msg);
}
final Charge captured;
synchronized (lock) {
captured = charge.capture();
}
if (captured.getStatus() == null) {
final String msg = "charge: no status returned for Charge, accountPlan=" + accountPlanUuid + " with paymentMethod=" + paymentMethodUuid + " and bill=" + billUuid;
log.error(msg);
throw invalidEx("err.purchase.cardUnknownError", msg);
} else {
final String msg;
switch (captured.getStatus()) {
case "succeeded":
log.info("charge: charge successful: "+authCacheKey);
chargeCache.set(billUuid, TRUE.toString(), "EX", CHARGE_CACHE_DURATION);
return;

case "pending":
msg = "charge: status='pending' (expected 'succeeded'), accountPlan=" + accountPlanUuid + " with paymentMethod=" + paymentMethodUuid + " and bill=" + billUuid;
log.error(msg);
throw invalidEx("err.purchase.chargePendingError", msg);

default:
msg = "charge: status='"+charge.getStatus()+"' (expected 'succeeded'), accountPlan=" + accountPlanUuid + " with paymentMethod=" + paymentMethodUuid + " and bill=" + billUuid;
log.error(msg);
throw invalidEx("err.purchase.chargeFailedError", msg);
}
}

} catch (CardException e) {
// The card has been declined
final String msg = "charge: CardException for accountPlan=" + accountPlanUuid + " with paymentMethod=" + paymentMethodUuid + " and bill=" + billUuid + ": requestId=" + e.getRequestId() + ", code="+e.getCode()+", declineCode="+e.getDeclineCode()+", error=" + e.toString();
log.error(msg);
throw invalidEx("err.purchase.cardError", msg);

} catch (StripeException e) {
final String msg = "charge: "+e.getClass().getSimpleName()+" for accountPlan=" + accountPlanUuid + " with paymentMethod=" + paymentMethodUuid + " and bill=" + billUuid + ": requestId=" + e.getRequestId() + ", code="+e.getCode()+", error=" + e.toString();
log.error(msg);
throw invalidEx("err.purchase.cardProcessingError", msg);

} catch (Exception e) {
final String msg = "charge: "+e.getClass().getSimpleName()+" for accountPlan=" + accountPlanUuid + " with paymentMethod=" + paymentMethodUuid + " and bill=" + billUuid + ": error=" + e.toString();
log.error(msg);
throw invalidEx("err.purchase.cardUnknownError", msg);
}
}

}

+ 1
- 1
bubble-server/src/main/java/bubble/dao/bill/AccountPaymentMethodDAO.java 查看文件

@@ -1,6 +1,6 @@
package bubble.dao.bill;

import bubble.cloud.payment.PaymentValidationResult;
import bubble.notify.payment.PaymentValidationResult;
import bubble.dao.account.AccountOwnedEntityDAO;
import bubble.dao.cloud.CloudServiceDAO;
import bubble.model.bill.AccountPaymentMethod;


+ 48
- 4
bubble-server/src/main/java/bubble/dao/bill/AccountPlanDAO.java 查看文件

@@ -1,17 +1,27 @@
package bubble.dao.bill;

import bubble.cloud.payment.PaymentServiceDriver;
import bubble.dao.account.AccountOwnedEntityDAO;
import bubble.model.bill.AccountPlan;
import bubble.model.bill.AccountPlanPaymentMethod;
import bubble.dao.cloud.CloudServiceDAO;
import bubble.model.bill.*;
import bubble.model.cloud.CloudService;
import bubble.server.BubbleConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import static java.util.concurrent.TimeUnit.SECONDS;
import static org.cobbzilla.util.daemon.ZillaRuntime.background;
import static org.cobbzilla.util.system.Sleep.sleep;
import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx;

@Repository
public class AccountPlanDAO extends AccountOwnedEntityDAO<AccountPlan> {

@Autowired private AccountPlanPaymentMethodDAO accountPlanPaymentMethodDAO;
@Autowired private BubblePlanDAO planDAO;
@Autowired private BillDAO billDAO;
@Autowired private CloudServiceDAO cloudDAO;
@Autowired private BubbleConfiguration configuration;

public AccountPlan findByAccountAndNetwork(String accountUuid, String networkUuid) {
return findByUniqueFields("account", accountUuid, "network", networkUuid);
@@ -19,14 +29,48 @@ public class AccountPlanDAO extends AccountOwnedEntityDAO<AccountPlan> {

@Override public Object preCreate(AccountPlan accountPlan) {
if (!accountPlan.hasPaymentMethod()) throw invalidEx("err.paymentMethod.required");

final CloudService paymentService = cloudDAO.findByUuid(accountPlan.getPaymentMethod().getCloud());
if (paymentService == null) throw invalidEx("err.paymentService.notFound");

final PaymentServiceDriver paymentDriver = paymentService.getPaymentDriver(configuration);
if (paymentDriver.getPaymentMethodType().requiresAuth()) {
final BubblePlan plan = planDAO.findByUuid(accountPlan.getPlan());
paymentDriver.authorize(plan, accountPlan, accountPlan.getPaymentMethod());
}

return super.preCreate(accountPlan);
}

@Override public AccountPlan postCreate(AccountPlan accountPlan, Object context) {
final String accountPlanUuid = accountPlan.getUuid();
final String paymentMethodUuid = accountPlan.getPaymentMethod().getUuid();
accountPlanPaymentMethodDAO.create(new AccountPlanPaymentMethod()
.setAccount(accountPlan.getAccount())
.setAccountPlan(accountPlan.getUuid())
.setPaymentMethod(accountPlan.getPaymentMethod().getUuid()));
.setAccountPlan(accountPlanUuid)
.setPaymentMethod(paymentMethodUuid));

final BubblePlan plan = planDAO.findByUuid(accountPlan.getPlan());
final Bill bill = billDAO.create(new Bill()
.setAccount(accountPlan.getAccount())
.setPlan(plan.getUuid())
.setAccountPlan(accountPlanUuid)
.setPrice(plan.getPrice())
.setCurrency(plan.getCurrency())
.setPeriod(plan.getPeriod().currentPeriod())
.setQuantity(1L)
.setType(BillItemType.compute)
.setStatus(BillStatus.unpaid));
final String billUuid = bill.getUuid();

final CloudService paymentService = cloudDAO.findByUuid(accountPlan.getPaymentMethod().getCloud());
if (paymentService == null) throw invalidEx("err.paymentService.notFound");

final PaymentServiceDriver paymentDriver = paymentService.getPaymentDriver(configuration);
background(() -> {
sleep(SECONDS.toMillis(3), "AccountPlanDAO.postCreate: waiting to finalize purchase");
paymentDriver.purchase(accountPlanUuid, paymentMethodUuid, billUuid);
});
return super.postCreate(accountPlan, context);
}



+ 43
- 0
bubble-server/src/main/java/bubble/dao/bill/AccountPlanPaymentDAO.java 查看文件

@@ -0,0 +1,43 @@
package bubble.dao.bill;

import bubble.dao.account.AccountOwnedEntityDAO;
import bubble.model.bill.AccountPlanPayment;
import org.springframework.stereotype.Repository;

import java.util.List;

import static org.hibernate.criterion.Restrictions.*;

@Repository
public class AccountPlanPaymentDAO extends AccountOwnedEntityDAO<AccountPlanPayment> {

public AccountPlanPayment findByBill(String billUuid) { return findByUniqueField("bill", billUuid); }

public AccountPlanPayment findByAccountPaymentAndBill(String planPaymentMethod, String billUuid) {
return findByUniqueFields("planPaymentMethod", planPaymentMethod, "bill", billUuid);
}

public List<AccountPlanPayment> findByAccountPaymentMethodAndPeriodAndPriceAndCurrency(String paymentMethodUuid,
String billPeriod,
Long price,
String currency) {
return list(criteria().add(and(
eq("paymentMethod", paymentMethodUuid),
eq("period", billPeriod),
eq("currency", currency),
ge("amount", price)
)));
}

public List<AccountPlanPayment> findByAccountAndBill(String accountUuid, String billUuid) {
return findByFields("account", accountUuid, "bill", billUuid);
}

public List<AccountPlanPayment> findByAccountAndAccountPlan(String accountUuid, String accountPlanUuid) {
return findByFields("account", accountUuid, "accountPlan", accountPlanUuid);
}

public List<AccountPlanPayment> findByAccountAndAccountPlanAndBill(String accountUuid, String accountPlanUuid, String billUuid) {
return findByFields("account", accountUuid, "accountPlan", accountPlanUuid, "bill", billUuid);
}
}

+ 1
- 1
bubble-server/src/main/java/bubble/dao/bill/AccountPlanPaymentMethodDAO.java 查看文件

@@ -1,6 +1,6 @@
package bubble.dao.bill;

import bubble.cloud.payment.PaymentValidationResult;
import bubble.notify.payment.PaymentValidationResult;
import bubble.dao.account.AccountOwnedEntityDAO;
import bubble.dao.cloud.CloudServiceDAO;
import bubble.model.bill.AccountPaymentMethod;


+ 2
- 2
bubble-server/src/main/java/bubble/dao/bill/BillDAO.java 查看文件

@@ -9,8 +9,8 @@ import java.util.List;
@Repository
public class BillDAO extends AccountOwnedEntityDAO<Bill> {

public List<Bill> findByAccountAndPlan(String accountUuid, String accountPlanUuid) {
return findByFields("account", accountUuid, "plan", accountPlanUuid);
public List<Bill> findByAccountAndAccountPlan(String accountUuid, String accountPlanUuid) {
return findByFields("account", accountUuid, "accountPlan", accountPlanUuid);
}

}

+ 38
- 19
bubble-server/src/main/java/bubble/model/bill/AccountPayment.java 查看文件

@@ -2,24 +2,27 @@ package bubble.model.bill;

import bubble.model.account.Account;
import bubble.model.account.HasAccountNoName;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.cobbzilla.wizard.model.IdentifiableBase;
import org.cobbzilla.wizard.model.entityconfig.annotations.ECForeignKey;
import org.cobbzilla.wizard.model.entityconfig.annotations.ECType;
import org.cobbzilla.wizard.model.entityconfig.annotations.ECTypeFields;
import org.cobbzilla.wizard.model.entityconfig.annotations.ECTypeURIs;
import org.cobbzilla.wizard.model.entityconfig.annotations.*;
import org.cobbzilla.wizard.validation.MultiViolationException;
import org.cobbzilla.wizard.validation.SimpleViolationException;
import org.hibernate.annotations.Type;

import javax.persistence.*;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;

import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.ENCRYPTED_STRING;
import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.ENC_PAD;
import static org.cobbzilla.util.daemon.ZillaRuntime.errorString;
import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.*;

@ECType(root=true) @ECTypeURIs(listFields={"account", "plan", "paymentMethod", "bill"})
@ECTypeFields(list={"account", "plan", "paymentMethod", "bill"})
@ECType(root=true) @ECTypeURIs(listFields={"account", "paymentMethod", "amount"})
@ECTypeFields(list={"account", "paymentMethod", "amount"})
@Entity @NoArgsConstructor @Accessors(chain=true)
public class AccountPayment extends IdentifiableBase implements HasAccountNoName {

@@ -27,28 +30,44 @@ public class AccountPayment extends IdentifiableBase implements HasAccountNoName
@Column(nullable=false, updatable=false, length=UUID_MAXLEN)
@Getter @Setter private String account;

@ECForeignKey(entity=AccountPlan.class)
@Column(nullable=false, updatable=false, length=UUID_MAXLEN)
@Getter @Setter private String plan;

@ECForeignKey(entity=AccountPaymentMethod.class)
@Column(nullable=false, updatable=false, length=UUID_MAXLEN)
@Getter @Setter private String paymentMethod;

@ECForeignKey(entity=AccountPlanPaymentMethod.class)
@ECForeignKey(entity=BubblePlan.class)
@Column(nullable=false, updatable=false, length=UUID_MAXLEN)
@Getter @Setter private String planPaymentMethod;
@Getter @Setter private String plan;

@ECForeignKey(entity=Bill.class)
@ECForeignKey(entity=AccountPlan.class)
@Column(nullable=false, updatable=false, length=UUID_MAXLEN)
@Getter @Setter private String bill;
@Getter @Setter private String accountPlan;

@Enumerated(EnumType.STRING) @Column(nullable=false, length=20)
@Getter @Setter private AccountPaymentStatus status;

@Type(type=ENCRYPTED_STRING) @Column(updatable=false, columnDefinition="varchar("+(200+ENC_PAD)+")")
@Getter @Setter private String violation;

@Type(type=ENCRYPTED_STRING) @Column(updatable=false, columnDefinition="varchar("+(10000+ENC_PAD)+")")
@JsonIgnore @Getter @Setter private String exception;

public AccountPayment setError (Exception e) {
if (e instanceof SimpleViolationException) {
setViolation(((SimpleViolationException) e).getMessageTemplate());
} else if (e instanceof MultiViolationException) {
setViolation(((MultiViolationException) e).getViolations().get(0).getMessageTemplate());
}
setException(errorString(e));
return this;
}

@Type(type=ENCRYPTED_LONG) @Column(updatable=false, columnDefinition="varchar("+(ENC_LONG)+") NOT NULL")
@Getter @Setter private Long amount = 0L;

@ECIndex @Column(nullable=false, updatable=false, length=10)
@Getter @Setter private String currency;

@Type(type=ENCRYPTED_STRING) @Column(updatable=false, columnDefinition="varchar("+(100000+ENC_PAD)+") NOT NULL")
@Getter @Setter private String info;

@Transient @Getter @Setter private Bill billObject;

}

+ 1
- 1
bubble-server/src/main/java/bubble/model/bill/AccountPaymentMethod.java 查看文件

@@ -2,7 +2,7 @@ package bubble.model.bill;

import bubble.cloud.CloudServiceType;
import bubble.cloud.payment.PaymentServiceDriver;
import bubble.cloud.payment.PaymentValidationResult;
import bubble.notify.payment.PaymentValidationResult;
import bubble.dao.cloud.CloudServiceDAO;
import bubble.model.account.Account;
import bubble.model.account.HasAccountNoName;


+ 4
- 0
bubble-server/src/main/java/bubble/model/bill/AccountPlan.java 查看文件

@@ -58,6 +58,10 @@ public class AccountPlan extends IdentifiableBase implements HasAccount {
@Column(length=UUID_MAXLEN)
@Getter @Setter private String network;

@Column(nullable=false)
@Getter @Setter private Boolean enabled = false;
public boolean enabled() { return enabled != null && enabled; }

@ECIndex(unique=true) @Column(length=UUID_MAXLEN)
@Getter @Setter private String deletedNetwork;
public boolean deleted() { return deletedNetwork != null; }


+ 69
- 0
bubble-server/src/main/java/bubble/model/bill/AccountPlanPayment.java 查看文件

@@ -0,0 +1,69 @@
package bubble.model.bill;

import bubble.model.account.Account;
import bubble.model.account.HasAccountNoName;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.cobbzilla.wizard.model.IdentifiableBase;
import org.cobbzilla.wizard.model.entityconfig.annotations.*;
import org.hibernate.annotations.Type;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Transient;

import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.ENCRYPTED_LONG;
import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.ENC_LONG;

@ECType(root=true) @ECTypeURIs(listFields={"account", "plan", "accountPlan", "payment", "bill"})
@ECTypeFields(list={"account", "plan", "accountPlan", "payment", "bill"})
@ECIndexes({
@ECIndex(unique=true, of={"planPaymentMethod", "bill"})
})
@Entity @NoArgsConstructor @Accessors(chain=true)
public class AccountPlanPayment extends IdentifiableBase implements HasAccountNoName {

@ECForeignKey(entity=Account.class)
@Column(nullable=false, updatable=false, length=UUID_MAXLEN)
@Getter @Setter private String account;

@ECForeignKey(entity=BubblePlan.class)
@Column(nullable=false, updatable=false, length=UUID_MAXLEN)
@Getter @Setter private String plan;

@ECForeignKey(entity=AccountPlan.class)
@Column(nullable=false, updatable=false, length=UUID_MAXLEN)
@Getter @Setter private String accountPlan;

@ECForeignKey(entity=AccountPayment.class)
@Column(nullable=false, updatable=false, length=UUID_MAXLEN)
@Getter @Setter private String payment;

@ECForeignKey(entity=AccountPaymentMethod.class)
@Column(nullable=false, updatable=false, length=UUID_MAXLEN)
@Getter @Setter private String paymentMethod;

@ECForeignKey(entity=AccountPlanPaymentMethod.class)
@Column(nullable=false, updatable=false, length=UUID_MAXLEN)
@Getter @Setter private String planPaymentMethod;

@ECIndex(unique=true) @ECForeignKey(index=false, entity=Bill.class)
@Column(nullable=false, updatable=false, length=UUID_MAXLEN)
@Getter @Setter private String bill;

@Column(nullable=false, updatable=false, length=50)
@ECIndex @Getter @Setter private String period;

@Type(type=ENCRYPTED_LONG) @Column(updatable=false, columnDefinition="varchar("+(ENC_LONG)+") NOT NULL")
@Getter @Setter private Long amount = 0L;

@ECIndex @Column(nullable=false, updatable=false, length=10)
@Getter @Setter private String currency;

@Transient @Getter @Setter private transient AccountPlan accountPlanObject;
@Transient @Getter @Setter private transient AccountPayment paymentObject;
@Transient @Getter @Setter private transient Bill billObject;

}

+ 14
- 6
bubble-server/src/main/java/bubble/model/bill/Bill.java 查看文件

@@ -16,8 +16,8 @@ import javax.persistence.*;
import static org.cobbzilla.util.daemon.ZillaRuntime.big;
import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.*;

@ECType(root=true) @ECTypeURIs(listFields={"name", "type", "quantity", "price", "period"})
@ECTypeFields(list={"name", "type", "quantity", "price", "period"})
@ECType(root=true) @ECTypeURIs(listFields={"name", "status", "type", "quantity", "price", "period"})
@ECTypeFields(list={"name", "Status", "type", "quantity", "price", "period"})
@Entity @NoArgsConstructor @Accessors(chain=true)
@ECIndexes({
@ECIndex(unique=true, of={"account", "plan", "type", "period"})
@@ -28,22 +28,30 @@ public class Bill extends IdentifiableBase implements HasAccountNoName {
@Column(nullable=false, updatable=false, length=UUID_MAXLEN)
@Getter @Setter private String account;

@ECForeignKey(entity=AccountPlan.class)
@ECForeignKey(entity=BubblePlan.class)
@Column(nullable=false, updatable=false, length=UUID_MAXLEN)
@Getter @Setter private String plan;

@ECForeignKey(entity=AccountPlan.class)
@Column(nullable=false, updatable=false, length=UUID_MAXLEN)
@Getter @Setter private String accountPlan;

@ECIndex @Enumerated(EnumType.STRING)
@Column(nullable=false, length=20)
@Getter @Setter private BillStatus status = BillStatus.unpaid;

@ECIndex @Enumerated(EnumType.STRING)
@Column(nullable=false, updatable=false, length=20)
@Getter @Setter private BillItemType type;

@Column(nullable=false, updatable=false, length=10)
@Column(nullable=false, updatable=false, length=50)
@ECIndex @Getter @Setter private String period;

@Type(type=ENCRYPTED_LONG) @Column(updatable=false, columnDefinition="varchar("+(ENC_LONG)+") NOT NULL")
@Getter @Setter private Long quantity = 0L;

@Type(type=ENCRYPTED_INTEGER) @Column(updatable=false, columnDefinition="varchar("+(ENC_INT)+") NOT NULL")
@Getter @Setter private Integer price = 0;
@Type(type=ENCRYPTED_LONG) @Column(updatable=false, columnDefinition="varchar("+(ENC_LONG)+") NOT NULL")
@Getter @Setter private Long price = 0L;

@ECIndex @Column(nullable=false, updatable=false, length=10)
@Getter @Setter private String currency;


+ 10
- 1
bubble-server/src/main/java/bubble/model/bill/BillPeriod.java 查看文件

@@ -1,13 +1,22 @@
package bubble.model.bill;

import com.fasterxml.jackson.annotation.JsonCreator;
import lombok.AllArgsConstructor;
import org.joda.time.format.DateTimeFormatter;

import static bubble.ApiConstants.enumFromString;
import static org.cobbzilla.util.daemon.ZillaRuntime.now;
import static org.cobbzilla.util.time.TimeUtil.DATE_FORMAT_YYYY_MM;

@AllArgsConstructor
public enum BillPeriod {

monthly;
monthly (DATE_FORMAT_YYYY_MM);

private DateTimeFormatter formatter;

@JsonCreator public static BillPeriod fromString (String v) { return enumFromString(BillPeriod.class, v); }

public String currentPeriod() { return formatter.print(now()); }

}

+ 13
- 0
bubble-server/src/main/java/bubble/model/bill/BillStatus.java 查看文件

@@ -0,0 +1,13 @@
package bubble.model.bill;

import com.fasterxml.jackson.annotation.JsonCreator;

import static bubble.ApiConstants.enumFromString;

public enum BillStatus {

unpaid, paid;

@JsonCreator public static BillStatus fromString (String v) { return enumFromString(BillStatus.class, v); }

}

+ 1
- 1
bubble-server/src/main/java/bubble/model/bill/BubblePlan.java 查看文件

@@ -73,7 +73,7 @@ public class BubblePlan extends IdentifiableBase implements HasAccount {
public boolean enabled () { return enabled == null || enabled; }

@ECIndex @Column(nullable=false)
@Getter @Setter private Integer price;
@Getter @Setter private Long price;

@ECIndex @Column(nullable=false, length=10)
@Getter @Setter private String currency = "USD";


+ 2
- 1
bubble-server/src/main/java/bubble/model/bill/PaymentMethodType.java 查看文件

@@ -6,9 +6,10 @@ import static bubble.ApiConstants.enumFromString;

public enum PaymentMethodType {

credit, paypal, code, free;
credit, code, free;

public boolean requiresClaim() { return this == code; }
public boolean requiresAuth() { return this == credit; }

@JsonCreator public static PaymentMethodType fromString(String v) { return enumFromString(PaymentMethodType.class, v); }



+ 5
- 3
bubble-server/src/main/java/bubble/model/cloud/notify/NotificationType.java 查看文件

@@ -5,9 +5,10 @@ import bubble.cloud.compute.ComputeNodeSize;
import bubble.cloud.geoCode.GeoCodeResult;
import bubble.cloud.geoLocation.GeoLocation;
import bubble.cloud.geoTime.GeoTimeZone;
import bubble.cloud.payment.PaymentValidationResult;
import bubble.model.cloud.BubbleNode;
import bubble.notify.ReceivedNotificationHandler;
import bubble.notify.payment.PaymentResult;
import bubble.notify.payment.PaymentValidationResult;
import bubble.notify.storage.StorageResult;
import bubble.server.BubbleConfiguration;
import com.fasterxml.jackson.annotation.JsonCreator;
@@ -89,8 +90,9 @@ public enum NotificationType {
// delegated payment driver notifications
payment_driver_validate (PaymentValidationResult.class),
payment_driver_claim (PaymentValidationResult.class),
payment_driver_purchase (Boolean.class),
payment_driver_refund (Boolean.class),
payment_driver_authorize (PaymentResult.class),
payment_driver_purchase (PaymentResult.class),
payment_driver_refund (PaymentResult.class),
payment_driver_response (true);

private String packageName = "bubble.notify";


+ 1
- 0
bubble-server/src/main/java/bubble/notify/NewNodeNotification.java 查看文件

@@ -10,6 +10,7 @@ import static org.cobbzilla.util.daemon.ZillaRuntime.empty;
@NoArgsConstructor @Accessors(chain=true)
public class NewNodeNotification {

@Getter @Setter private String account;
@Getter @Setter private String host;
@Getter @Setter private String network;
@Getter @Setter private String domain;


+ 1
- 0
bubble-server/src/main/java/bubble/notify/NotificationHandler_hello_from_sage.java 查看文件

@@ -107,6 +107,7 @@ public class NotificationHandler_hello_from_sage extends ReceivedNotificationHan
final CloudService cloud = closestNotUs.getCloud();

final NewNodeNotification newNodeRequest = new NewNodeNotification()
.setAccount(network.getAccount())
.setNetwork(network.getUuid())
.setDomain(network.getDomain())
.setCloud(cloud.getUuid())


+ 12
- 9
bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver.java 查看文件

@@ -1,9 +1,7 @@
package bubble.notify.payment;

import bubble.dao.bill.AccountPaymentMethodDAO;
import bubble.dao.cloud.BubbleNodeDAO;
import bubble.dao.cloud.CloudServiceDAO;
import bubble.model.bill.AccountPaymentMethod;
import bubble.model.cloud.BubbleNode;
import bubble.model.cloud.CloudService;
import bubble.model.cloud.notify.ReceivedNotification;
@@ -17,7 +15,6 @@ import static org.cobbzilla.util.json.JsonUtil.json;
public abstract class NotificationHandler_payment_driver extends DelegatedNotificationHandlerBase {

@Autowired protected BubbleNodeDAO nodeDAO;
@Autowired protected AccountPaymentMethodDAO paymentMethodDAO;
@Autowired protected CloudServiceDAO cloudDAO;

@Override public void handleNotification(ReceivedNotification n) {
@@ -25,12 +22,18 @@ public abstract class NotificationHandler_payment_driver extends DelegatedNotifi
if (sender == null) die("sender not found: "+n.getFromNode());

final PaymentNotification paymentNotification = json(n.getPayloadJson(), PaymentNotification.class);
final AccountPaymentMethod paymentMethod = paymentMethodDAO.findByUuid(paymentNotification.getPaymentMethodUuid());

final CloudService paymentService = cloudDAO.findByAccountAndName(configuration.getThisNode().getAccount(), paymentMethod.getCloud());
final boolean success = handlePaymentRequest(paymentNotification, paymentService);

notifySender(payment_driver_response, n.getNotificationId(), sender, success);
final CloudService paymentService = cloudDAO.findByAccountAndName(configuration.getThisNode().getAccount(), paymentNotification.getCloud());
PaymentResult result;
try {
if (handlePaymentRequest(paymentNotification, paymentService)) {
result = PaymentResult.SUCCESS;
} else {
result = PaymentResult.FAILURE;
}
} catch (Exception e) {
result = PaymentResult.exception(e);
}
notifySender(payment_driver_response, n.getNotificationId(), sender, result);
}

protected abstract boolean handlePaymentRequest(PaymentNotification paymentNotification,


+ 26
- 0
bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_authorize.java 查看文件

@@ -0,0 +1,26 @@
package bubble.notify.payment;

import bubble.dao.bill.AccountPaymentMethodDAO;
import bubble.dao.bill.AccountPlanDAO;
import bubble.dao.bill.BubblePlanDAO;
import bubble.model.bill.AccountPaymentMethod;
import bubble.model.bill.AccountPlan;
import bubble.model.bill.BubblePlan;
import bubble.model.cloud.CloudService;
import org.springframework.beans.factory.annotation.Autowired;

public class NotificationHandler_payment_driver_authorize extends NotificationHandler_payment_driver {

@Autowired private BubblePlanDAO planDAO;
@Autowired private AccountPaymentMethodDAO paymentMethodDAO;
@Autowired private AccountPlanDAO accountPlanDAO;

@Override public boolean handlePaymentRequest(PaymentNotification paymentNotification, CloudService paymentService) {
// todo: this returns null because the AccountPlan has not yet been created....
final AccountPlan accountPlan = accountPlanDAO.findByUuid(paymentNotification.getAccountPlanUuid());
final BubblePlan plan = planDAO.findByUuid(accountPlan.getPlan());
final AccountPaymentMethod paymentMethod = paymentMethodDAO.findByUuid(paymentNotification.getPaymentMethodUuid());
return paymentService.getPaymentDriver(configuration).authorize(plan, accountPlan, paymentMethod);
}

}

+ 0
- 1
bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_claim.java 查看文件

@@ -1,7 +1,6 @@
package bubble.notify.payment;

import bubble.cloud.payment.PaymentServiceDriver;
import bubble.cloud.payment.PaymentValidationResult;
import bubble.dao.cloud.BubbleNodeDAO;
import bubble.dao.cloud.CloudServiceDAO;
import bubble.model.cloud.BubbleNode;


+ 1
- 3
bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_purchase.java 查看文件

@@ -8,9 +8,7 @@ public class NotificationHandler_payment_driver_purchase extends NotificationHan
return paymentService.getPaymentDriver(configuration).purchase(
paymentNotification.getAccountPlanUuid(),
paymentNotification.getPaymentMethodUuid(),
paymentNotification.getBillUuid(),
paymentNotification.getPurchaseAmount(),
paymentNotification.getCurrency()
paymentNotification.getBillUuid()
);
}



+ 1
- 2
bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_refund.java 查看文件

@@ -9,8 +9,7 @@ public class NotificationHandler_payment_driver_refund extends NotificationHandl
paymentNotification.getAccountPlanUuid(),
paymentNotification.getPaymentMethodUuid(),
paymentNotification.getBillUuid(),
paymentNotification.getPurchaseAmount(),
paymentNotification.getCurrency()
paymentNotification.getAmount()
);
}



+ 0
- 1
bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_validate.java 查看文件

@@ -1,6 +1,5 @@
package bubble.notify.payment;

import bubble.cloud.payment.PaymentValidationResult;
import bubble.dao.cloud.BubbleNodeDAO;
import bubble.dao.cloud.CloudServiceDAO;
import bubble.model.cloud.BubbleNode;


+ 2
- 2
bubble-server/src/main/java/bubble/notify/payment/PaymentMethodClaimNotification.java 查看文件

@@ -19,12 +19,12 @@ public class PaymentMethodClaimNotification extends SynchronousNotification {

@Getter @Setter private String cloud;

public PaymentMethodClaimNotification(AccountPaymentMethod paymentMethod, String cloud) {
public PaymentMethodClaimNotification(String cloud, AccountPaymentMethod paymentMethod) {
this.paymentMethod = paymentMethod;
this.cloud = cloud;
}

public PaymentMethodClaimNotification(AccountPlanPaymentMethod planPaymentMethod, String cloud) {
public PaymentMethodClaimNotification(String cloud, AccountPlanPaymentMethod planPaymentMethod) {
this.planPaymentMethod = planPaymentMethod;
this.cloud = cloud;
}


+ 1
- 1
bubble-server/src/main/java/bubble/notify/payment/PaymentMethodValidationNotification.java 查看文件

@@ -11,7 +11,7 @@ import lombok.experimental.Accessors;
@NoArgsConstructor @AllArgsConstructor @Accessors(chain=true)
public class PaymentMethodValidationNotification extends SynchronousNotification {

@Getter @Setter private AccountPaymentMethod paymentMethod;
@Getter @Setter private String cloud;
@Getter @Setter private AccountPaymentMethod paymentMethod;

}

+ 3
- 4
bubble-server/src/main/java/bubble/notify/payment/PaymentNotification.java 查看文件

@@ -1,19 +1,18 @@
package bubble.notify.payment;

import bubble.notify.SynchronousNotification;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.Accessors;

@NoArgsConstructor @AllArgsConstructor @Accessors(chain=true)
@NoArgsConstructor @Accessors(chain=true)
public class PaymentNotification extends SynchronousNotification {

@Getter @Setter private String cloud;
@Getter @Setter private String accountPlanUuid;
@Getter @Setter private String paymentMethodUuid;
@Getter @Setter private String billUuid;
@Getter @Setter private int purchaseAmount;
@Getter @Setter private String currency;
@Getter @Setter private long amount;

}

+ 50
- 0
bubble-server/src/main/java/bubble/notify/payment/PaymentResult.java 查看文件

@@ -0,0 +1,50 @@
package bubble.notify.payment;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.cobbzilla.wizard.validation.ConstraintViolationBean;
import org.cobbzilla.wizard.validation.MultiViolationException;
import org.cobbzilla.wizard.validation.SimpleViolationException;

import java.util.Arrays;
import java.util.List;

import static org.cobbzilla.util.daemon.ZillaRuntime.empty;
import static org.cobbzilla.util.daemon.ZillaRuntime.errorString;

@NoArgsConstructor @Accessors(chain=true)
public class PaymentResult {

public static final PaymentResult SUCCESS = new PaymentResult().setSuccess(true);
public static final PaymentResult FAILURE = new PaymentResult().setSuccess(false);

@Getter @Setter private Boolean success;
public boolean success() { return success != null && success; }

@Getter @Setter private ConstraintViolationBean[] violations;
public boolean hasViolations() { return !empty(violations); }
public List<ConstraintViolationBean> violationList() { return Arrays.asList(getViolations()); }

@Getter @Setter private String error;
public boolean hasError() { return !empty(error); }

public static PaymentResult exception(Exception e) {
if (e instanceof SimpleViolationException) {
return new PaymentResult()
.setSuccess(false)
.setViolations(new ConstraintViolationBean[]{((SimpleViolationException) e).getBean()});

} else if (e instanceof MultiViolationException) {
return new PaymentResult()
.setSuccess(false)
.setViolations(((MultiViolationException) e).getViolations().toArray(ConstraintViolationBean.EMPTY_VIOLATION_ARRAY));
} else {
return new PaymentResult()
.setSuccess(false)
.setError(errorString(e));
}
}

}

bubble-server/src/main/java/bubble/cloud/payment/PaymentValidationResult.java → bubble-server/src/main/java/bubble/notify/payment/PaymentValidationResult.java 查看文件

@@ -1,4 +1,4 @@
package bubble.cloud.payment;
package bubble.notify.payment;

import bubble.model.bill.AccountPaymentMethod;
import lombok.Getter;
@@ -19,6 +19,7 @@ public class PaymentValidationResult {
@Getter @Setter private AccountPaymentMethod paymentMethod;

@Getter @Setter private ConstraintViolationBean[] violations;
public boolean hasViolations() { return !empty(violations); }
public List<ConstraintViolationBean> violationsList() { return Arrays.asList(violations); }

public PaymentValidationResult(AccountPaymentMethod paymentMethod) { this.paymentMethod = paymentMethod; }

+ 18
- 0
bubble-server/src/main/java/bubble/resources/account/ReadOnlyAccountOwnedResource.java 查看文件

@@ -0,0 +1,18 @@
package bubble.resources.account;

import bubble.dao.account.AccountOwnedEntityDAO;
import bubble.model.account.Account;
import bubble.model.account.HasAccount;
import org.glassfish.grizzly.http.server.Request;
import org.glassfish.jersey.server.ContainerRequest;

public class ReadOnlyAccountOwnedResource<E extends HasAccount, DAO extends AccountOwnedEntityDAO<E>>
extends AccountOwnedResource<E, DAO> {

public ReadOnlyAccountOwnedResource(Account account) { super(account); }

@Override protected boolean canCreate(Request req, ContainerRequest ctx, Account caller, E request) { return false; }
@Override protected boolean canUpdate(ContainerRequest ctx, Account caller, E found, E request) { return false; }
@Override protected boolean canDelete(ContainerRequest ctx, Account caller, E found) { return false; }

}

+ 2
- 38
bubble-server/src/main/java/bubble/resources/bill/AccountPaymentsResource.java 查看文件

@@ -3,55 +3,19 @@ package bubble.resources.bill;
import bubble.dao.bill.AccountPaymentDAO;
import bubble.model.account.Account;
import bubble.model.bill.AccountPayment;
import bubble.model.bill.AccountPlan;
import bubble.model.bill.Bill;
import bubble.resources.account.AccountOwnedResource;
import bubble.resources.account.ReadOnlyAccountOwnedResource;
import lombok.extern.slf4j.Slf4j;
import org.glassfish.grizzly.http.server.Request;
import org.glassfish.jersey.server.ContainerRequest;

import javax.ws.rs.Consumes;
import javax.ws.rs.Produces;
import java.util.List;

import static org.cobbzilla.util.http.HttpContentTypes.APPLICATION_JSON;

@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
@Slf4j
public class AccountPaymentsResource extends AccountOwnedResource<AccountPayment, AccountPaymentDAO> {

private AccountPlan accountPlan;
private Bill bill;
public class AccountPaymentsResource extends ReadOnlyAccountOwnedResource<AccountPayment, AccountPaymentDAO> {

public AccountPaymentsResource(Account account) { super(account); }

public AccountPaymentsResource(Account account, AccountPlan accountPlan) {
super(account);
this.accountPlan = accountPlan;
}

public AccountPaymentsResource(Account account, Bill bill) {
super(account);
this.bill = bill;
}

@Override protected boolean canCreate(Request req, ContainerRequest ctx, Account caller, AccountPayment request) { return false; }
@Override protected boolean canUpdate(ContainerRequest ctx, Account caller, AccountPayment found, AccountPayment request) { return false; }
@Override protected boolean canDelete(ContainerRequest ctx, Account caller, AccountPayment found) { return false; }

@Override protected AccountPayment find(ContainerRequest ctx, String id) {
final AccountPayment payment = super.find(ctx, id);
return payment == null
|| (accountPlan != null && !payment.getPlan().equals(accountPlan.getUuid()))
|| (bill != null && !payment.getBill().equals(bill.getUuid()))
? null : payment;
}

@Override protected List<AccountPayment> list(ContainerRequest ctx) {
if (accountPlan == null && bill == null) return super.list(ctx);
if (bill != null) return getDao().findByAccountAndBill(getAccountUuid(ctx), bill.getUuid());
return getDao().findByAccountAndPlan(getAccountUuid(ctx), accountPlan.getUuid());
}

}

+ 96
- 0
bubble-server/src/main/java/bubble/resources/bill/AccountPlanPaymentsResource.java 查看文件

@@ -0,0 +1,96 @@
package bubble.resources.bill;

import bubble.dao.bill.AccountPaymentDAO;
import bubble.dao.bill.AccountPlanDAO;
import bubble.dao.bill.AccountPlanPaymentDAO;
import bubble.dao.bill.BillDAO;
import bubble.model.account.Account;
import bubble.model.bill.AccountPayment;
import bubble.model.bill.AccountPlan;
import bubble.model.bill.AccountPlanPayment;
import bubble.model.bill.Bill;
import bubble.resources.account.ReadOnlyAccountOwnedResource;
import lombok.extern.slf4j.Slf4j;
import org.glassfish.jersey.server.ContainerRequest;
import org.springframework.beans.factory.annotation.Autowired;

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_BILL;
import static bubble.ApiConstants.EP_PAYMENT;
import static org.cobbzilla.wizard.resources.ResourceUtil.*;

@Slf4j
public class AccountPlanPaymentsResource extends ReadOnlyAccountOwnedResource<AccountPlanPayment, AccountPlanPaymentDAO> {

@Autowired private AccountPlanDAO accountPlanDAO;
@Autowired private AccountPaymentDAO paymentDAO;
@Autowired private BillDAO billDAO;

private Bill bill;
private AccountPlan accountPlan;

public AccountPlanPaymentsResource(Account account) { super(account); }

public AccountPlanPaymentsResource(Account account, Bill bill) {
this(account);
this.bill = bill;
}

public AccountPlanPaymentsResource(Account account, AccountPlan accountPlan) {
this(account);
this.accountPlan = accountPlan;
}

@Override protected AccountPlanPayment find(ContainerRequest ctx, String id) {
final AccountPlanPayment planPayment = super.find(ctx, id);
if (bill != null && !planPayment.getBill().equals(bill.getUuid())) return null;
if (accountPlan != null && !planPayment.getAccountPlan().equals(accountPlan.getUuid())) return null;
return planPayment;
}

@Override protected List<AccountPlanPayment> list(ContainerRequest ctx) {
if (bill == null) {
if (accountPlan == null) {
return super.list(ctx);
} else {
return getDao().findByAccountAndAccountPlan(getAccountUuid(ctx), accountPlan.getUuid());
}
} else if (accountPlan != null) {
return getDao().findByAccountAndAccountPlanAndBill(getAccountUuid(ctx), accountPlan.getUuid(), bill.getUuid());

} else {
return getDao().findByAccountAndBill(getAccountUuid(ctx), bill.getUuid());
}
}

@Override protected AccountPlanPayment populate(ContainerRequest ctx, AccountPlanPayment planPayment) {
planPayment.setAccountPlanObject(accountPlan != null ? accountPlan : accountPlanDAO.findByUuid(planPayment.getAccountPlan()));
planPayment.setPaymentObject(paymentDAO.findByUuid(planPayment.getPayment()));
planPayment.setBillObject(bill != null ? null : billDAO.findByUuid(planPayment.getBill()));
return planPayment;
}

@Path("/{id}"+EP_PAYMENT)
public Response getPayment(@Context ContainerRequest ctx,
@PathParam("id") String id) {
final AccountPlanPayment planPayment = super.find(ctx, id);
if (planPayment == null) throw notFoundEx(id);
final AccountPayment payment = paymentDAO.findByUuid(planPayment.getPayment());
return payment == null ? notFound(id) : ok(payment);
}

@Path("/{id}"+EP_BILL)
public Response getBill(@Context ContainerRequest ctx,
@PathParam("id") String id) {
final AccountPlanPayment planPayment = super.find(ctx, id);
if (planPayment == null) throw notFoundEx(id);
final Bill bill = billDAO.findByUuid(planPayment.getBill());
return bill == null ? notFound(id) : ok(bill);
}

}

+ 2
- 2
bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java 查看文件

@@ -62,11 +62,11 @@ public class AccountPlansResource extends AccountOwnedResource<AccountPlan, Acco
}

@Path("/{id}"+EP_PAYMENTS)
public AccountPaymentsResource getPayments(@Context ContainerRequest ctx,
public AccountPlanPaymentsResource getPayments(@Context ContainerRequest ctx,
@PathParam("id") String id) {
final AccountPlan plan = find(ctx, id);
if (plan == null) throw notFoundEx(id);
return configuration.subResource(AccountPaymentsResource.class, account, plan);
return configuration.subResource(AccountPlanPaymentsResource.class, account, plan);
}

@GET @Path("/{id}"+EP_PAYMENT_METHOD)


+ 6
- 11
bubble-server/src/main/java/bubble/resources/bill/BillsResource.java 查看文件

@@ -4,9 +4,8 @@ import bubble.dao.bill.BillDAO;
import bubble.model.account.Account;
import bubble.model.bill.AccountPlan;
import bubble.model.bill.Bill;
import bubble.resources.account.AccountOwnedResource;
import bubble.resources.account.ReadOnlyAccountOwnedResource;
import lombok.extern.slf4j.Slf4j;
import org.glassfish.grizzly.http.server.Request;
import org.glassfish.jersey.server.ContainerRequest;

import javax.ws.rs.Path;
@@ -18,7 +17,7 @@ import static bubble.ApiConstants.EP_PAYMENTS;
import static org.cobbzilla.wizard.resources.ResourceUtil.notFoundEx;

@Slf4j
public class BillsResource extends AccountOwnedResource<Bill, BillDAO> {
public class BillsResource extends ReadOnlyAccountOwnedResource<Bill, BillDAO> {

private AccountPlan accountPlan;

@@ -29,26 +28,22 @@ public class BillsResource extends AccountOwnedResource<Bill, BillDAO> {
this.accountPlan = accountPlan;
}

@Override protected boolean canCreate(Request req, ContainerRequest ctx, Account caller, Bill request) { return false; }
@Override protected boolean canUpdate(ContainerRequest ctx, Account caller, Bill found, Bill request) { return false; }
@Override protected boolean canDelete(ContainerRequest ctx, Account caller, Bill found) { return false; }

@Override protected Bill find(ContainerRequest ctx, String id) {
final Bill bill = super.find(ctx, id);
return bill == null || (accountPlan != null && !bill.getPlan().equals(accountPlan.getUuid())) ? null : bill;
return bill == null || (accountPlan != null && !bill.getAccountPlan().equals(accountPlan.getUuid())) ? null : bill;
}

@Override protected List<Bill> list(ContainerRequest ctx) {
if (accountPlan == null) return super.list(ctx);
return getDao().findByAccountAndPlan(getAccountUuid(ctx), accountPlan.getUuid());
return getDao().findByAccountAndAccountPlan(getAccountUuid(ctx), accountPlan.getUuid());
}

@Path("/{id}"+EP_PAYMENTS)
public AccountPaymentsResource getPayments(@Context ContainerRequest ctx,
public AccountPlanPaymentsResource getPayments(@Context ContainerRequest ctx,
@PathParam("id") String id) {
final Bill bill = super.find(ctx, id);
if (bill == null) throw notFoundEx(id);
return configuration.subResource(AccountPaymentsResource.class, account, bill);
return configuration.subResource(AccountPlanPaymentsResource.class, account, bill);
}

}

+ 2
- 18
bubble-server/src/main/java/bubble/resources/cloud/NodesResource.java 查看文件

@@ -5,9 +5,8 @@ import bubble.model.account.Account;
import bubble.model.cloud.BubbleDomain;
import bubble.model.cloud.BubbleNetwork;
import bubble.model.cloud.BubbleNode;
import bubble.resources.account.AccountOwnedResource;
import bubble.resources.account.ReadOnlyAccountOwnedResource;
import lombok.extern.slf4j.Slf4j;
import org.glassfish.grizzly.http.server.Request;
import org.glassfish.jersey.server.ContainerRequest;

import java.util.List;
@@ -15,7 +14,7 @@ import java.util.List;
import static org.cobbzilla.wizard.resources.ResourceUtil.forbiddenEx;

@Slf4j
public class NodesResource extends AccountOwnedResource<BubbleNode, BubbleNodeDAO> {
public class NodesResource extends ReadOnlyAccountOwnedResource<BubbleNode, BubbleNodeDAO> {

private BubbleNetwork network;
private BubbleDomain domain;
@@ -60,21 +59,6 @@ public class NodesResource extends AccountOwnedResource<BubbleNode, BubbleNodeDA
return node;
}

@Override protected boolean canCreate(Request req, ContainerRequest ctx, Account caller, BubbleNode request) {
// creation is done via starting or expanding the BubbleNetwork
return false;
}

@Override protected boolean canUpdate(ContainerRequest ctx, Account caller, BubbleNode found, BubbleNode request) {
// network creates first node, then nodes update themselves
return false;
}

@Override protected boolean canDelete(ContainerRequest ctx, Account caller, BubbleNode found) {
// deletion is done via stopping or shrinking the network
return false;
}

// these should never get called
@Override protected BubbleNode setReferences(ContainerRequest ctx, Account caller, BubbleNode node) { throw forbiddenEx(); }
@Override protected Object daoCreate(BubbleNode nodes) { throw forbiddenEx(); }


+ 12
- 1
bubble-server/src/main/java/bubble/service/cloud/StandardNetworkService.java 查看文件

@@ -125,8 +125,14 @@ public class StandardNetworkService implements NetworkService {
final BubbleDomain domain = domainDAO.findByUuid(network.getDomain());
final Account account = accountDAO.findByUuid(network.getAccount());

// enforce network size limit, if this is an automated request
final AccountPlan accountPlan = accountPlanDAO.findByAccountAndNetwork(account.getUuid(), network.getUuid());

// ensure AccountPlan has been paid for
if (!accountPlan.enabled()) {
return die("newNode: accountPlan is not enabled: "+accountPlan.getUuid());
}

// enforce network size limit, if this is an automated request
final BubblePlan plan = planDAO.findByUuid(accountPlan.getPlan());
final List<BubbleNode> peers = nodeDAO.findByAccountAndNetwork(account.getUuid(), network.getUuid());
if (peers.size() >= plan.getNodesIncluded() && nn.automated()) {
@@ -425,6 +431,7 @@ public class StandardNetworkService implements NetworkService {
final CloudAndRegion cloudAndRegion = geoService.selectCloudAndRegion(network, netLocation);
final String host = network.fork() ? network.getForkHost() : newNodeHostname();
final NewNodeNotification newNodeRequest = new NewNodeNotification()
.setAccount(network.getAccount())
.setNetwork(network.getUuid())
.setDomain(network.getDomain())
.setFork(network.fork())
@@ -475,6 +482,7 @@ public class StandardNetworkService implements NetworkService {
final String restoreKey = randomAlphanumeric(RESTORE_KEY_LEN).toUpperCase();
restoreService.registerRestore(restoreKey, new NetworkKeys());
final NewNodeNotification newNodeRequest = new NewNodeNotification()
.setAccount(network.getAccount())
.setNetwork(network.getUuid())
.setDomain(network.getDomain())
.setRestoreKey(restoreKey)
@@ -499,6 +507,9 @@ public class StandardNetworkService implements NetworkService {
}

public void backgroundNewNode(NewNodeNotification newNodeRequest, final String existingLock) {
final AccountPlan accountPlan = accountPlanDAO.findByAccountAndNetwork(newNodeRequest.getAccount(), newNodeRequest.getNetwork());
if (accountPlan == null) throw invalidEx("err.accountPlan.notFound");
if (!accountPlan.enabled()) throw invalidEx("err.accountPlan.disabled");
final AtomicReference<String> lock = new AtomicReference<>(existingLock);
daemon(new NodeLauncher(newNodeRequest, lock, this));
}


+ 4
- 0
bubble-server/src/main/resources/message_templates/server/en_US/post_auth/ResourceMessages.properties 查看文件

@@ -71,6 +71,8 @@ payment_description_free=Enjoy Bubble for FREE!
# Error messages from API server
err.accountContactsJson.length=Account contacts length violation
err.accountPlan.callerCountryDisallowed=Your country is not currently supported by our platform
err.accountPlan.disabled=Account plan is not enabled
err.accountPlan.notFound=Account plan not found
err.admin.cannotRemoveAdminStatusFromSelf=You cannot remove admin status from your own account
err.allowedCountriesJson.length=Allowed countries list is too long
err.approval.invalid=Approval cannot proceed
@@ -179,6 +181,7 @@ err.privateKeyHash.length=Private key hash is too long
err.privateKey.length=Private key is too long
err.purchase.accountMismatch=Invalid payment
err.purchase.amountMismatch=Payment amount does not match billed amount
err.purchase.authNotFound=Payment authorization was not found
err.purchase.billNotFound=Bill not found
err.purchase.cardError=Error processing credit card payment
err.purchase.cardProcessingError=Error processing credit card payment
@@ -195,6 +198,7 @@ err.purchase.tokenExpired=Invite code has expired
err.purchase.tokenUsed=Invite code has already been used
err.purchase.tokenInvalid=Error reading invite code
err.purchase.tokenMismatch=Error reading invite code
err.purchase.underReview=Charge is currently under review
err.purchase.declined=Purchase was unsuccessful
err.quantity.length=Quantity is too long
err.region.required=Region is required


+ 3
- 10
bubble-server/src/test/java/bubble/test/PaymentTest.java 查看文件

@@ -2,14 +2,9 @@ package bubble.test;

import bubble.server.BubbleConfiguration;
import lombok.extern.slf4j.Slf4j;
import org.cobbzilla.util.http.PhantomUtil;
import org.cobbzilla.wizard.server.RestServer;
import org.junit.Test;

import java.util.concurrent.TimeUnit;

import static org.cobbzilla.util.system.Sleep.sleep;

@Slf4j
public class PaymentTest extends ActivatedBubbleModelTestBase {

@@ -27,11 +22,9 @@ public class PaymentTest extends ActivatedBubbleModelTestBase {
@Test public void testCodePayment () throws Exception { modelTest("payment/pay_code"); }
@Test public void testCreditPayment () throws Exception { modelTest("payment/pay_credit"); }

@Test public void testPhantom () throws Exception {
log.info("go for it!");
sleep(TimeUnit.MINUTES.toMillis(60));
PhantomUtil.init();
// new PhantomUtil().loadPageAndExec(getConfiguration().getPublicUriBase()+"/stripe/index.html", "");
@Test public void testCreditPaymentMultipleStartStop () throws Exception {
modelTest("payment/pay_credit_multi_start_stop");
}


}

+ 12
- 3
bubble-server/src/test/resources/models/tests/payment/pay_code.json 查看文件

@@ -30,6 +30,15 @@
}
},

{
"comment": "get plans",
"request": { "uri": "plans" },
"response": {
"store": "plans",
"check": [{"condition": "json.length >= 1"}]
}
},

{
"comment": "add plan, using 'code' payment method but with an invalid code",
"request": {
@@ -40,7 +49,7 @@
"domain": "{{defaultDomain}}",
"locale": "en_US",
"timezone": "EST",
"plan": "bubble",
"plan": "{{plans.[0].name}}",
"footprint": "US",
"paymentMethod": {
"paymentMethodType": "code",
@@ -66,7 +75,7 @@
"domain": "{{defaultDomain}}",
"locale": "en_US",
"timezone": "EST",
"plan": "bubble",
"plan": "{{plans.[0].name}}",
"footprint": "US",
"paymentMethod": {
"paymentMethodType": "code",
@@ -122,7 +131,7 @@
"domain": "{{defaultDomain}}",
"locale": "en_US",
"timezone": "EST",
"plan": "bubble",
"plan": "{{plans.[0].name}}",
"footprint": "US",
"paymentMethod": {
"paymentMethodType": "code",


+ 16
- 5
bubble-server/src/test/resources/models/tests/payment/pay_credit.json 查看文件

@@ -31,7 +31,16 @@
},

{
"comment": "get payment methods, so we can get the stripe public key to tokenize a credit card",
"comment": "get plans",
"request": { "uri": "plans" },
"response": {
"store": "plans",
"check": [{"condition": "json.length >= 1"}]
}
},

{
"comment": "get payment methods, tokenize a credit card",
"request": { "uri": "paymentMethods" },
"response": {
"store": "paymentMethods"
@@ -49,7 +58,7 @@
"domain": "{{defaultDomain}}",
"locale": "en_US",
"timezone": "EST",
"plan": "bubble",
"plan": "{{plans.[0].name}}",
"footprint": "US",
"paymentMethod": {
"paymentMethodType": "credit",
@@ -75,7 +84,7 @@
"domain": "{{defaultDomain}}",
"locale": "en_US",
"timezone": "EST",
"plan": "bubble",
"plan": "{{plans.[0].name}}",
"footprint": "US",
"paymentMethod": {
"paymentMethodType": "credit",
@@ -127,7 +136,7 @@
"domain": "{{defaultDomain}}",
"locale": "en_US",
"timezone": "EST",
"plan": "bubble",
"plan": "{{plans.[0].name}}",
"footprint": "US",
"paymentMethod": {
"paymentMethodType": "credit",
@@ -152,12 +161,14 @@
},

{
"before": "sleep 5s",
"comment": "verify account plans, should be one",
"request": { "uri": "me/plans" },
"response": {
"check": [
{"condition": "json.length == 1"},
{"condition": "json[0].getName() == accountPlan.getName()"}
{"condition": "json[0].getName() == accountPlan.getName()"},
{"condition": "json[0].enabled()"}
]
}
},


+ 332
- 0
bubble-server/src/test/resources/models/tests/payment/pay_credit_multi_start_stop.json 查看文件

@@ -0,0 +1,332 @@
[
{
"comment": "create a user account",
"request": {
"uri": "users",
"method": "put",
"entity": {
"name": "test_user",
"password": "password",
"contact": {"type": "email", "info": "test-user@example.com"}
}
}
},

{
"before": "sleep 22s", // wait for account objects to be created
"comment": "login as new user",
"request": {
"session": "new",
"uri": "auth/login",
"entity": {
"name": "test_user",
"password": "password"
}
},
"response": {
"store": "testAccount",
"sessionName": "userSession",
"session": "token"
}
},

{
"comment": "get payment methods, tokenize a credit card",
"request": { "uri": "paymentMethods" },
"response": {
"store": "paymentMethods"
},
"after": "stripe_tokenize_card"
},

{
"before": "sleep 1s",
"comment": "as root, check email inbox for verification message",
"request": {
"session": "rootSession",
"uri": "debug/inbox/email/test-user@example.com?type=request&action=verify&target=account"
},
"response": {
"store": "emailInbox",
"check": [
{"condition": "'{{json.[0].ctx.message.messageType}}' === 'request'"},
{"condition": "'{{json.[0].ctx.message.action}}' === 'verify'"},
{"condition": "'{{json.[0].ctx.message.target}}' === 'account'"}
]
}
},

{
"comment": "approve email verification request",
"request": {
"session": "userSession",
"uri": "auth/approve/{{emailInbox.[0].ctx.confirmationToken}}",
"method": "post"
}
},

{
"comment": "get plans",
"request": { "uri": "plans" },
"response": {
"store": "plans",
"check": [{"condition": "json.length >= 1"}]
}
},

{
"comment": "add plan, using 'credit' payment method with a valid card token, creates a stripe customer",
"request": {
"uri": "me/plans",
"method": "put",
"entity": {
"name": "test-net-{{rand 5}}",
"domain": "{{defaultDomain}}",
"locale": "en_US",
"timezone": "EST",
"plan": "{{plans.[0].name}}",
"footprint": "US",
"paymentMethod": {
"paymentMethodType": "credit",
"paymentInfo": "{{stripeToken}}"
}
}
},
"response": {
"store": "accountPlan"
}
},

{
"comment": "verify account payment methods, should be one",
"request": { "uri": "me/paymentMethods" },
"response": {
"check": [
{"condition": "json.length === 1"},
{"condition": "json[0].getPaymentMethodType().name() === 'credit'"}
]
}
},

{
"comment": "verify account plans, should be one",
"request": { "uri": "me/plans" },
"response": {
"check": [
{"condition": "json.length === 1"},
{"condition": "json[0].getName() === accountPlan.getName()"}
]
}
},

{
"comment": "verify account plan payment info",
"request": { "uri": "me/plans/{{accountPlan.uuid}}/paymentMethod" },
"response": {
"store": "savedPaymentMethods",
"check": [
{"condition": "json.getPaymentMethodType().name() === 'credit'"},
{"condition": "json.getMaskedPaymentInfo() === 'XXXX-XXXX-XXXX-4242'"}
]
}
},

{
"comment": "verify bill exists for new service with correct price and has been paid",
"request": { "uri": "me/bills" },
"response": {
"store": "bills",
"check": [
{"condition": "json.length === 1"},
{"condition": "json[0].getPlan() === '{{accountPlan.uuid}}'"},
{"condition": "json[0].getQuantity() === 1"},
{"condition": "json[0].getPrice() === {{plans.[0].price}}"},
{"condition": "json[0].getTotal() === {{plans.[0].price}}"},
{"condition": "json[0].getStatus().name() === 'paid'"}
]
}
},

{
"comment": "verify successful payment exists for new service",
"request": { "uri": "me/payments" },
"response": {
"store": "payments",
"check": [
{"condition": "json.length === 1"},
{"condition": "json[0].getPlan() === '{{accountPlan.uuid}}'"},
{"condition": "json[0].getAmount() === '{{plans.[0].price}}'"},
{"condition": "json[0].getStatus().name() === 'success'"}
]
}
},

{
"comment": "verify successful payment has paid for the bill above",
"request": { "uri": "me/payments/{{payments.[0].uuid}}/bills" },
"response": {
"check": [
{"condition": "json.length === 1"},
{"condition": "json[0].getUuid() === '{{bills.[0].uuid}}'"}
]
}
},

{
"comment": "delete plan",
"request": {
"method": "delete",
"uri": "me/plans/{{accountPlan.uuid}}"
}
},

{
"comment": "verify no plans exist",
"request": { "uri": "me/plans" },
"response": {
"check": [ {"condition": "json.length === 0"} ]
}
},

{
"comment": "add a second plan, using saved payment method",
"request": {
"uri": "me/plans",
"method": "put",
"entity": {
"name": "test-net2-{{rand 5}}",
"domain": "{{defaultDomain}}",
"locale": "en_US",
"timezone": "EST",
"plan": "{{plans.[0].name}}",
"footprint": "US",
"paymentMethod": {
"paymentMethodType": "credit",
"uuid": "{{savedPaymentMethods.[0].uuid}}"
}
}
},
"response": {
"store": "accountPlan2"
}
},

{
"comment": "verify we still have only one payment method",
"request": { "uri": "me/paymentMethods" },
"response": {
"check": [ {"condition": "json.length === 1"} ]
}
},

{
"comment": "verify account plan payment info is same as used before",
"request": { "uri": "me/plans/{{accountPlan.uuid}}/paymentMethod" },
"response": {
"check": [
{"condition": "json.getPaymentMethodType().name() === 'credit'"},
{"condition": "json.getMaskedPaymentInfo() === 'XXXX-XXXX-XXXX-4242'"},
{"condition": "json.getUuid() === '{{savedPaymentMethods.[0].uuid}}'"}
]
}
},

{
"comment": "verify we now have two bills",
"request": { "uri": "me/bills" },
"response": {
"check": [
{"condition": "json.length === 2"}
]
}
},

{
"comment": "verify bill exists for new service, but price is zero",
"request": { "uri": "me/plans/{{accountPlan2.uuid}}/bills" },
"response": {
"store": "plan2bills",
"check": [
{"condition": "json.length === 1"},
{"condition": "json[0].getPlan() === '{{accountPlan2.uuid}}'"},
{"condition": "json[0].getQuantity() === 1"},
{"condition": "json[0].getPrice() === 0"},
{"condition": "json[0].getTotal() === 0"}
]
}
},

{
"comment": "verify we have made two successful payments (but one of them will be zero)",
"request": { "uri": "me/payments" },
"response": {
"check": [
{"condition": "json.length === 2"},
{"condition": "json[0].getStatus().name() === 'success'"},
{"condition": "json[1].getStatus().name() === 'success'"},
{"condition": "_find(json, function (p) { if (p.getAmount() === {{plans.[0].price}}) return p; }) != null"},
{"condition": "_find(json, function (p) { if (p.getAmount() === 0) return p; }) != null"}
]
}
},

{
"comment": "find payments made on second plan",
"request": { "uri": "me/plans/{{accountPlan2.uuid}}/payments" },
"response": {
"store": "plan2payments",
"check": [
{"condition": "json.length === 1"}
]
}
},

{
"comment": "verify successful payment has paid for the second bill (with zero total)",
"request": { "uri": "me/plans/{{accountPlan2.uuid}}/payments/{{plan2payments.[0].uuid}}/bills" },
"response": {
"check": [
{"condition": "json.length === 1"},
{"condition": "json[0].getPlan() === '{{accountPlan2.uuid}}'"},
{"condition": "json[0].getQuantity() === 1"},
{"condition": "json[0].getPrice() === 0"},
{"condition": "json[0].getTotal() === 0"}
]
}
}

// todo: add a third plan, this one will require a second payment since two plans are now active

// todo: remove third plan. now only one plan is active

// todo: fast-forward 32 days, trigger BillGenerator

// todo: verify a new Bill exists for accountPlan2, and payment has been made successfully

// todo: set mock such that charging the card fails

// todo: fast-forward 32 days, trigger BillGenerator

// todo: verify a new Bill exists for accountPlan2 and Bill remains 'unpaid', and AccountPayment failed

// todo: verify payment reminder messages have been sent

// todo: fast-forward 1 day, trigger BillGenerator

// todo: verify a new Bill exists for accountPlan2 and Bill remains 'unpaid', and AccountPayment failed

// todo: verify payment reminder messages have been sent

// todo: fast-forward 3 days, trigger BillGenerator.

// todo: verify a new Bill exists for accountPlan2 and Bill remains 'unpaid', and AccountPayment failed

// todo: verify network associated with plan has been stopped

// todo: try to start network, fails due to non-payment

// todo: submit payment

// todo: start network, succeeds

]

+ 88
- 11
bubble-server/src/test/resources/models/tests/payment/pay_free.json 查看文件

@@ -30,6 +30,15 @@
}
},

{
"comment": "get plans",
"request": { "uri": "plans" },
"response": {
"store": "plans",
"check": [{"condition": "json.length >= 1"}]
}
},

{
"comment": "add plan, fails because we have not supplied payment information",
"request": {
@@ -40,7 +49,7 @@
"domain": "{{defaultDomain}}",
"locale": "en_US",
"timezone": "EST",
"plan": "bubble",
"plan": "{{plans.[0].name}}",
"footprint": "US"
}
},
@@ -62,7 +71,7 @@
"domain": "{{defaultDomain}}",
"locale": "en_US",
"timezone": "EST",
"plan": "bubble",
"plan": "{{plans.[0].name}}",
"footprint": "US",
"paymentMethod": {
"paymentInfo": "free"
@@ -87,7 +96,7 @@
"domain": "{{defaultDomain}}",
"locale": "en_US",
"timezone": "EST",
"plan": "bubble",
"plan": "{{plans.[0].name}}",
"footprint": "US",
"paymentMethod": {
"paymentMethodType": "free"
@@ -112,7 +121,7 @@
"domain": "{{defaultDomain}}",
"locale": "en_US",
"timezone": "EST",
"plan": "bubble",
"plan": "{{plans.[0].name}}",
"footprint": "US",
"paymentMethod": {
"paymentMethodType": "free",
@@ -130,19 +139,21 @@
"request": { "uri": "me/paymentMethods" },
"response": {
"check": [
{"condition": "json.length == 1"},
{"condition": "json[0].getPaymentMethodType().name() == 'free'"}
{"condition": "json.length === 1"},
{"condition": "json[0].getPaymentMethodType().name() === 'free'"}
]
}
},

{
"comment": "verify account plans, should be one",
"before": "sleep 15s",
"comment": "verify account plans, should be one, verify enabled",
"request": { "uri": "me/plans" },
"response": {
"check": [
{"condition": "json.length == 1"},
{"condition": "json[0].getName() == accountPlan.getName()"}
{"condition": "json.length === 1"},
{"condition": "json[0].getName() === accountPlan.getName()"},
{"condition": "json[0].enabled()"}
]
}
},
@@ -152,8 +163,74 @@
"request": { "uri": "me/plans/{{accountPlan.uuid}}/paymentMethod" },
"response": {
"check": [
{"condition": "json.getPaymentMethodType().name() == 'free'"},
{"condition": "json.getMaskedPaymentInfo() == 'XXXXXXXX'"}
{"condition": "json.getPaymentMethodType().name() === 'free'"},
{"condition": "json.getMaskedPaymentInfo() === 'XXXXXXXX'"}
]
}
},

{
"comment": "verify bill exists and is paid",
"request": { "uri": "me/bills" },
"response": {
"check": [
{"condition": "json.length === 1"},
{"condition": "json[0].getPlan() === '{{plans.[0].uuid}}'"},
{"condition": "json[0].getAccountPlan() === '{{accountPlan.uuid}}'"},
{"condition": "json[0].getQuantity() === 1"},
{"condition": "json[0].getPrice() === {{plans.[0].price}}"},
{"condition": "json[0].getTotal() === {{plans.[0].price}}"},
{"condition": "json[0].getStatus().name() === 'paid'"}
]
}
},

{
"comment": "verify bill exists via plan and is paid",
"request": { "uri": "me/plans/{{accountPlan.uuid}}/bills" },
"response": {
"check": [
{"condition": "json.length === 1"},
{"condition": "json[0].getPlan() === '{{plans.[0].uuid}}'"},
{"condition": "json[0].getAccountPlan() === '{{accountPlan.uuid}}'"},
{"condition": "json[0].getQuantity() === 1"},
{"condition": "json[0].getPrice() === {{plans.[0].price}}"},
{"condition": "json[0].getTotal() === {{plans.[0].price}}"},
{"condition": "json[0].getStatus().name() === 'paid'"}
]
}
},

{
"comment": "verify payment exists and is successful",
"request": { "uri": "me/payments" },
"response": {
"check": [
{"condition": "json.length === 1"},
{"condition": "json[0].getPlan() === '{{plans.[0].uuid}}'"},
{"condition": "json[0].getAccountPlan() === '{{accountPlan.uuid}}'"},
{"condition": "json[0].getAmount() === {{plans.[0].price}}"},
{"condition": "json[0].getStatus().name() === 'success'"}
]
}
},

{
"comment": "verify payment exists via plan and is successful",
"request": { "uri": "me/plans/{{accountPlan.uuid}}/payments" },
"response": {
"check": [
{"condition": "json.length === 1"},
{"condition": "json[0].getPlan() === '{{plans.[0].uuid}}'"},
{"condition": "json[0].getAccountPlan() === '{{accountPlan.uuid}}'"},
{"condition": "json[0].getPaymentObject().getAmount() === {{plans.[0].price}}"},
{"condition": "json[0].getPaymentObject().getStatus().name() === 'success'"},
{"condition": "json[0].getBillObject().getPlan() === '{{plans.[0].uuid}}'"},
{"condition": "json[0].getBillObject().getAccountPlan() === '{{accountPlan.uuid}}'"},
{"condition": "json[0].getBillObject().getQuantity() === 1"},
{"condition": "json[0].getBillObject().getPrice() === {{plans.[0].price}}"},
{"condition": "json[0].getBillObject().getTotal() === {{plans.[0].price}}"},
{"condition": "json[0].getBillObject().getStatus().name() === 'paid'"}
]
}
}

+ 1
- 1
utils/cobbzilla-utils

@@ -1 +1 @@
Subproject commit f182aa1e314b2105ebf789eba1b1b1f003ffd765
Subproject commit 03f16e3d52cc380068a20da8136228fbf10cb7c7

Loading…
取消
儲存