@@ -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; | |||
@@ -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())); | |||
} | |||
} |
@@ -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); | |||
} |
@@ -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"); | |||
} | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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 | |||
} | |||
} |
@@ -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,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; | |||
@@ -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); | |||
} | |||
@@ -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,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; | |||
@@ -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); | |||
} | |||
} |
@@ -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; | |||
} |
@@ -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; | |||
@@ -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; } | |||
@@ -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; | |||
} |
@@ -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; | |||
@@ -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()); } | |||
} |
@@ -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); } | |||
} |
@@ -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"; | |||
@@ -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,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"; | |||
@@ -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; | |||
@@ -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()) | |||
@@ -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, | |||
@@ -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); | |||
} | |||
} |
@@ -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; | |||
@@ -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() | |||
); | |||
} | |||
@@ -9,8 +9,7 @@ public class NotificationHandler_payment_driver_refund extends NotificationHandl | |||
paymentNotification.getAccountPlanUuid(), | |||
paymentNotification.getPaymentMethodUuid(), | |||
paymentNotification.getBillUuid(), | |||
paymentNotification.getPurchaseAmount(), | |||
paymentNotification.getCurrency() | |||
paymentNotification.getAmount() | |||
); | |||
} | |||
@@ -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; | |||
@@ -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; | |||
} | |||
@@ -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; | |||
} |
@@ -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; | |||
} |
@@ -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)); | |||
} | |||
} | |||
} |
@@ -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; } |
@@ -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; } | |||
} |
@@ -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()); | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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) | |||
@@ -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); | |||
} | |||
} |
@@ -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(); } | |||
@@ -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)); | |||
} | |||
@@ -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 | |||
@@ -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"); | |||
} | |||
} |
@@ -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", | |||
@@ -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()"} | |||
] | |||
} | |||
}, | |||
@@ -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 | |||
] |
@@ -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 @@ | |||
Subproject commit f182aa1e314b2105ebf789eba1b1b1f003ffd765 | |||
Subproject commit 03f16e3d52cc380068a20da8136228fbf10cb7c7 |