From 0485123632c814fc42a72503a69912c8c1fca09c Mon Sep 17 00:00:00 2001 From: Jonathan Cobb Date: Fri, 13 Dec 2019 23:08:02 -0500 Subject: [PATCH] WIP. developing authorize step prior to capture. --- .../src/main/java/bubble/ApiConstants.java | 2 + .../cloud/payment/PaymentDriverBase.java | 177 ++++++++++ .../cloud/payment/PaymentServiceDriver.java | 12 +- .../cloud/payment/code/CodePaymentDriver.java | 94 ++--- .../delegate/DelegatedPaymentDriver.java | 61 +++- .../cloud/payment/free/FreePaymentDriver.java | 19 +- .../payment/stripe/StripePaymentDriver.java | 173 ++++++--- .../dao/bill/AccountPaymentMethodDAO.java | 2 +- .../java/bubble/dao/bill/AccountPlanDAO.java | 52 ++- .../dao/bill/AccountPlanPaymentDAO.java | 43 +++ .../dao/bill/AccountPlanPaymentMethodDAO.java | 2 +- .../main/java/bubble/dao/bill/BillDAO.java | 4 +- .../bubble/model/bill/AccountPayment.java | 57 ++- .../model/bill/AccountPaymentMethod.java | 2 +- .../java/bubble/model/bill/AccountPlan.java | 4 + .../bubble/model/bill/AccountPlanPayment.java | 69 ++++ .../src/main/java/bubble/model/bill/Bill.java | 20 +- .../java/bubble/model/bill/BillPeriod.java | 11 +- .../java/bubble/model/bill/BillStatus.java | 13 + .../java/bubble/model/bill/BubblePlan.java | 2 +- .../bubble/model/bill/PaymentMethodType.java | 3 +- .../model/cloud/notify/NotificationType.java | 8 +- .../bubble/notify/NewNodeNotification.java | 1 + .../NotificationHandler_hello_from_sage.java | 1 + .../NotificationHandler_payment_driver.java | 21 +- ...ationHandler_payment_driver_authorize.java | 26 ++ ...ificationHandler_payment_driver_claim.java | 1 - ...cationHandler_payment_driver_purchase.java | 4 +- ...ficationHandler_payment_driver_refund.java | 3 +- ...cationHandler_payment_driver_validate.java | 1 - .../PaymentMethodClaimNotification.java | 4 +- .../PaymentMethodValidationNotification.java | 2 +- .../notify/payment/PaymentNotification.java | 7 +- .../bubble/notify/payment/PaymentResult.java | 50 +++ .../payment/PaymentValidationResult.java | 3 +- .../account/ReadOnlyAccountOwnedResource.java | 18 + .../bill/AccountPaymentsResource.java | 40 +-- .../bill/AccountPlanPaymentsResource.java | 96 +++++ .../resources/bill/AccountPlansResource.java | 4 +- .../bubble/resources/bill/BillsResource.java | 17 +- .../bubble/resources/cloud/NodesResource.java | 20 +- .../service/cloud/StandardNetworkService.java | 13 +- .../post_auth/ResourceMessages.properties | 4 + .../test/java/bubble/test/PaymentTest.java | 13 +- .../models/tests/payment/pay_code.json | 15 +- .../models/tests/payment/pay_credit.json | 21 +- .../payment/pay_credit_multi_start_stop.json | 332 ++++++++++++++++++ .../models/tests/payment/pay_free.json | 99 +++++- utils/cobbzilla-utils | 2 +- 49 files changed, 1321 insertions(+), 327 deletions(-) create mode 100644 bubble-server/src/main/java/bubble/dao/bill/AccountPlanPaymentDAO.java create mode 100644 bubble-server/src/main/java/bubble/model/bill/AccountPlanPayment.java create mode 100644 bubble-server/src/main/java/bubble/model/bill/BillStatus.java create mode 100644 bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_authorize.java create mode 100644 bubble-server/src/main/java/bubble/notify/payment/PaymentResult.java rename bubble-server/src/main/java/bubble/{cloud => notify}/payment/PaymentValidationResult.java (92%) create mode 100644 bubble-server/src/main/java/bubble/resources/account/ReadOnlyAccountOwnedResource.java create mode 100644 bubble-server/src/main/java/bubble/resources/bill/AccountPlanPaymentsResource.java create mode 100644 bubble-server/src/test/resources/models/tests/payment/pay_credit_multi_start_stop.json diff --git a/bubble-server/src/main/java/bubble/ApiConstants.java b/bubble-server/src/main/java/bubble/ApiConstants.java index 198620cf..8b99795e 100644 --- a/bubble-server/src/main/java/bubble/ApiConstants.java +++ b/bubble-server/src/main/java/bubble/ApiConstants.java @@ -120,7 +120,9 @@ public class ApiConstants { public static final String EP_VPN = "/vpn"; public static final String EP_PAYMENT_METHOD = "/paymentMethod"; public static final String EP_PAYMENT_METHODS = PAYMENT_METHODS_ENDPOINT; + public static final String EP_PAYMENT = "/payment"; public static final String EP_PAYMENTS = "/payments"; + public static final String EP_BILL = "/bill"; public static final String EP_BILLS = "/bills"; public static final String EP_CLOSEST = "/closest"; public static final String EP_ROLES = ROLES_ENDPOINT; diff --git a/bubble-server/src/main/java/bubble/cloud/payment/PaymentDriverBase.java b/bubble-server/src/main/java/bubble/cloud/payment/PaymentDriverBase.java index 8ca530ac..ef00a807 100644 --- a/bubble-server/src/main/java/bubble/cloud/payment/PaymentDriverBase.java +++ b/bubble-server/src/main/java/bubble/cloud/payment/PaymentDriverBase.java @@ -2,9 +2,186 @@ package bubble.cloud.payment; import bubble.cloud.CloudServiceDriverBase; import bubble.cloud.CloudServiceType; +import bubble.dao.bill.*; +import bubble.model.bill.*; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import java.util.List; + +import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx; + +@Slf4j public abstract class PaymentDriverBase extends CloudServiceDriverBase 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 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())); + } + } diff --git a/bubble-server/src/main/java/bubble/cloud/payment/PaymentServiceDriver.java b/bubble-server/src/main/java/bubble/cloud/payment/PaymentServiceDriver.java index c9accc2a..dc83eff5 100644 --- a/bubble-server/src/main/java/bubble/cloud/payment/PaymentServiceDriver.java +++ b/bubble-server/src/main/java/bubble/cloud/payment/PaymentServiceDriver.java @@ -1,9 +1,8 @@ package bubble.cloud.payment; import bubble.cloud.CloudServiceType; -import bubble.model.bill.AccountPaymentMethod; -import bubble.model.bill.AccountPlanPaymentMethod; -import bubble.model.bill.PaymentMethodType; +import bubble.model.bill.*; +import bubble.notify.payment.PaymentValidationResult; import static org.cobbzilla.util.daemon.ZillaRuntime.notSupported; @@ -18,10 +17,9 @@ public interface PaymentServiceDriver { default PaymentValidationResult claim(AccountPaymentMethod paymentMethod) { return notSupported("claim"); } default PaymentValidationResult claim(AccountPlanPaymentMethod planPaymentMethod) { return notSupported("claim"); } - boolean purchase(String accountPlanUuid, String paymentMethodUuid, String billUuid, - int purchaseAmount, String currency); + boolean authorize(BubblePlan plan, AccountPlan accountPlan, AccountPaymentMethod paymentMethod); - boolean refund(String accountPlanUuid, String paymentMethodUuid, String billUuid, - int refundAmount, String currency); + boolean purchase(String accountPlanUuid, String paymentMethodUuid, String billUuid); + boolean refund(String accountPlanUuid, String paymentMethodUuid, String billUuid, long refundAmount); } diff --git a/bubble-server/src/main/java/bubble/cloud/payment/code/CodePaymentDriver.java b/bubble-server/src/main/java/bubble/cloud/payment/code/CodePaymentDriver.java index 773bc309..dbb2220c 100644 --- a/bubble-server/src/main/java/bubble/cloud/payment/code/CodePaymentDriver.java +++ b/bubble-server/src/main/java/bubble/cloud/payment/code/CodePaymentDriver.java @@ -2,7 +2,7 @@ package bubble.cloud.payment.code; import bubble.cloud.payment.DefaultPaymentDriverConfig; import bubble.cloud.payment.PaymentDriverBase; -import bubble.cloud.payment.PaymentValidationResult; +import bubble.notify.payment.PaymentValidationResult; import bubble.dao.bill.*; import bubble.dao.cloud.CloudServiceDAO; import bubble.dao.cloud.CloudServiceDataDAO; @@ -21,12 +21,7 @@ import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx; @Slf4j public class CodePaymentDriver extends PaymentDriverBase { - @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 { @@ -19,14 +18,12 @@ public class FreePaymentDriver extends PaymentDriverBase setupDone = new AtomicReference<>(null); @@ -123,94 +118,162 @@ public class StripePaymentDriver extends PaymentDriverBase 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); + } } } diff --git a/bubble-server/src/main/java/bubble/dao/bill/AccountPaymentMethodDAO.java b/bubble-server/src/main/java/bubble/dao/bill/AccountPaymentMethodDAO.java index 416b1d63..cb373768 100644 --- a/bubble-server/src/main/java/bubble/dao/bill/AccountPaymentMethodDAO.java +++ b/bubble-server/src/main/java/bubble/dao/bill/AccountPaymentMethodDAO.java @@ -1,6 +1,6 @@ package bubble.dao.bill; -import bubble.cloud.payment.PaymentValidationResult; +import bubble.notify.payment.PaymentValidationResult; import bubble.dao.account.AccountOwnedEntityDAO; import bubble.dao.cloud.CloudServiceDAO; import bubble.model.bill.AccountPaymentMethod; diff --git a/bubble-server/src/main/java/bubble/dao/bill/AccountPlanDAO.java b/bubble-server/src/main/java/bubble/dao/bill/AccountPlanDAO.java index b0a4ba86..172d0e8f 100644 --- a/bubble-server/src/main/java/bubble/dao/bill/AccountPlanDAO.java +++ b/bubble-server/src/main/java/bubble/dao/bill/AccountPlanDAO.java @@ -1,17 +1,27 @@ package bubble.dao.bill; +import bubble.cloud.payment.PaymentServiceDriver; import bubble.dao.account.AccountOwnedEntityDAO; -import bubble.model.bill.AccountPlan; -import bubble.model.bill.AccountPlanPaymentMethod; +import bubble.dao.cloud.CloudServiceDAO; +import bubble.model.bill.*; +import bubble.model.cloud.CloudService; +import bubble.server.BubbleConfiguration; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.cobbzilla.util.daemon.ZillaRuntime.background; +import static org.cobbzilla.util.system.Sleep.sleep; import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx; @Repository public class AccountPlanDAO extends AccountOwnedEntityDAO { @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 { @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); } diff --git a/bubble-server/src/main/java/bubble/dao/bill/AccountPlanPaymentDAO.java b/bubble-server/src/main/java/bubble/dao/bill/AccountPlanPaymentDAO.java new file mode 100644 index 00000000..040a211d --- /dev/null +++ b/bubble-server/src/main/java/bubble/dao/bill/AccountPlanPaymentDAO.java @@ -0,0 +1,43 @@ +package bubble.dao.bill; + +import bubble.dao.account.AccountOwnedEntityDAO; +import bubble.model.bill.AccountPlanPayment; +import org.springframework.stereotype.Repository; + +import java.util.List; + +import static org.hibernate.criterion.Restrictions.*; + +@Repository +public class AccountPlanPaymentDAO extends AccountOwnedEntityDAO { + + public AccountPlanPayment findByBill(String billUuid) { return findByUniqueField("bill", billUuid); } + + public AccountPlanPayment findByAccountPaymentAndBill(String planPaymentMethod, String billUuid) { + return findByUniqueFields("planPaymentMethod", planPaymentMethod, "bill", billUuid); + } + + public List 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 findByAccountAndBill(String accountUuid, String billUuid) { + return findByFields("account", accountUuid, "bill", billUuid); + } + + public List findByAccountAndAccountPlan(String accountUuid, String accountPlanUuid) { + return findByFields("account", accountUuid, "accountPlan", accountPlanUuid); + } + + public List findByAccountAndAccountPlanAndBill(String accountUuid, String accountPlanUuid, String billUuid) { + return findByFields("account", accountUuid, "accountPlan", accountPlanUuid, "bill", billUuid); + } +} diff --git a/bubble-server/src/main/java/bubble/dao/bill/AccountPlanPaymentMethodDAO.java b/bubble-server/src/main/java/bubble/dao/bill/AccountPlanPaymentMethodDAO.java index cd308042..395f6d19 100644 --- a/bubble-server/src/main/java/bubble/dao/bill/AccountPlanPaymentMethodDAO.java +++ b/bubble-server/src/main/java/bubble/dao/bill/AccountPlanPaymentMethodDAO.java @@ -1,6 +1,6 @@ package bubble.dao.bill; -import bubble.cloud.payment.PaymentValidationResult; +import bubble.notify.payment.PaymentValidationResult; import bubble.dao.account.AccountOwnedEntityDAO; import bubble.dao.cloud.CloudServiceDAO; import bubble.model.bill.AccountPaymentMethod; diff --git a/bubble-server/src/main/java/bubble/dao/bill/BillDAO.java b/bubble-server/src/main/java/bubble/dao/bill/BillDAO.java index a1e0fdf8..985916dc 100644 --- a/bubble-server/src/main/java/bubble/dao/bill/BillDAO.java +++ b/bubble-server/src/main/java/bubble/dao/bill/BillDAO.java @@ -9,8 +9,8 @@ import java.util.List; @Repository public class BillDAO extends AccountOwnedEntityDAO { - public List findByAccountAndPlan(String accountUuid, String accountPlanUuid) { - return findByFields("account", accountUuid, "plan", accountPlanUuid); + public List findByAccountAndAccountPlan(String accountUuid, String accountPlanUuid) { + return findByFields("account", accountUuid, "accountPlan", accountPlanUuid); } } diff --git a/bubble-server/src/main/java/bubble/model/bill/AccountPayment.java b/bubble-server/src/main/java/bubble/model/bill/AccountPayment.java index d3e14721..ba53afb2 100644 --- a/bubble-server/src/main/java/bubble/model/bill/AccountPayment.java +++ b/bubble-server/src/main/java/bubble/model/bill/AccountPayment.java @@ -2,24 +2,27 @@ package bubble.model.bill; import bubble.model.account.Account; import bubble.model.account.HasAccountNoName; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.experimental.Accessors; import org.cobbzilla.wizard.model.IdentifiableBase; -import org.cobbzilla.wizard.model.entityconfig.annotations.ECForeignKey; -import org.cobbzilla.wizard.model.entityconfig.annotations.ECType; -import org.cobbzilla.wizard.model.entityconfig.annotations.ECTypeFields; -import org.cobbzilla.wizard.model.entityconfig.annotations.ECTypeURIs; +import org.cobbzilla.wizard.model.entityconfig.annotations.*; +import org.cobbzilla.wizard.validation.MultiViolationException; +import org.cobbzilla.wizard.validation.SimpleViolationException; import org.hibernate.annotations.Type; -import javax.persistence.*; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; -import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.ENCRYPTED_STRING; -import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.ENC_PAD; +import static org.cobbzilla.util.daemon.ZillaRuntime.errorString; +import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.*; -@ECType(root=true) @ECTypeURIs(listFields={"account", "plan", "paymentMethod", "bill"}) -@ECTypeFields(list={"account", "plan", "paymentMethod", "bill"}) +@ECType(root=true) @ECTypeURIs(listFields={"account", "paymentMethod", "amount"}) +@ECTypeFields(list={"account", "paymentMethod", "amount"}) @Entity @NoArgsConstructor @Accessors(chain=true) public class AccountPayment extends IdentifiableBase implements HasAccountNoName { @@ -27,28 +30,44 @@ public class AccountPayment extends IdentifiableBase implements HasAccountNoName @Column(nullable=false, updatable=false, length=UUID_MAXLEN) @Getter @Setter private String account; - @ECForeignKey(entity=AccountPlan.class) - @Column(nullable=false, updatable=false, length=UUID_MAXLEN) - @Getter @Setter private String plan; - @ECForeignKey(entity=AccountPaymentMethod.class) @Column(nullable=false, updatable=false, length=UUID_MAXLEN) @Getter @Setter private String paymentMethod; - @ECForeignKey(entity=AccountPlanPaymentMethod.class) + @ECForeignKey(entity=BubblePlan.class) @Column(nullable=false, updatable=false, length=UUID_MAXLEN) - @Getter @Setter private String planPaymentMethod; + @Getter @Setter private String plan; - @ECForeignKey(entity=Bill.class) + @ECForeignKey(entity=AccountPlan.class) @Column(nullable=false, updatable=false, length=UUID_MAXLEN) - @Getter @Setter private String bill; + @Getter @Setter private String accountPlan; @Enumerated(EnumType.STRING) @Column(nullable=false, length=20) @Getter @Setter private AccountPaymentStatus status; + @Type(type=ENCRYPTED_STRING) @Column(updatable=false, columnDefinition="varchar("+(200+ENC_PAD)+")") + @Getter @Setter private String violation; + + @Type(type=ENCRYPTED_STRING) @Column(updatable=false, columnDefinition="varchar("+(10000+ENC_PAD)+")") + @JsonIgnore @Getter @Setter private String exception; + + public AccountPayment setError (Exception e) { + if (e instanceof SimpleViolationException) { + setViolation(((SimpleViolationException) e).getMessageTemplate()); + } else if (e instanceof MultiViolationException) { + setViolation(((MultiViolationException) e).getViolations().get(0).getMessageTemplate()); + } + setException(errorString(e)); + return this; + } + + @Type(type=ENCRYPTED_LONG) @Column(updatable=false, columnDefinition="varchar("+(ENC_LONG)+") NOT NULL") + @Getter @Setter private Long amount = 0L; + + @ECIndex @Column(nullable=false, updatable=false, length=10) + @Getter @Setter private String currency; + @Type(type=ENCRYPTED_STRING) @Column(updatable=false, columnDefinition="varchar("+(100000+ENC_PAD)+") NOT NULL") @Getter @Setter private String info; - @Transient @Getter @Setter private Bill billObject; - } diff --git a/bubble-server/src/main/java/bubble/model/bill/AccountPaymentMethod.java b/bubble-server/src/main/java/bubble/model/bill/AccountPaymentMethod.java index e8815efe..daf13f31 100644 --- a/bubble-server/src/main/java/bubble/model/bill/AccountPaymentMethod.java +++ b/bubble-server/src/main/java/bubble/model/bill/AccountPaymentMethod.java @@ -2,7 +2,7 @@ package bubble.model.bill; import bubble.cloud.CloudServiceType; import bubble.cloud.payment.PaymentServiceDriver; -import bubble.cloud.payment.PaymentValidationResult; +import bubble.notify.payment.PaymentValidationResult; import bubble.dao.cloud.CloudServiceDAO; import bubble.model.account.Account; import bubble.model.account.HasAccountNoName; diff --git a/bubble-server/src/main/java/bubble/model/bill/AccountPlan.java b/bubble-server/src/main/java/bubble/model/bill/AccountPlan.java index d558670a..2deb883f 100644 --- a/bubble-server/src/main/java/bubble/model/bill/AccountPlan.java +++ b/bubble-server/src/main/java/bubble/model/bill/AccountPlan.java @@ -58,6 +58,10 @@ public class AccountPlan extends IdentifiableBase implements HasAccount { @Column(length=UUID_MAXLEN) @Getter @Setter private String network; + @Column(nullable=false) + @Getter @Setter private Boolean enabled = false; + public boolean enabled() { return enabled != null && enabled; } + @ECIndex(unique=true) @Column(length=UUID_MAXLEN) @Getter @Setter private String deletedNetwork; public boolean deleted() { return deletedNetwork != null; } diff --git a/bubble-server/src/main/java/bubble/model/bill/AccountPlanPayment.java b/bubble-server/src/main/java/bubble/model/bill/AccountPlanPayment.java new file mode 100644 index 00000000..a39e3ac4 --- /dev/null +++ b/bubble-server/src/main/java/bubble/model/bill/AccountPlanPayment.java @@ -0,0 +1,69 @@ +package bubble.model.bill; + +import bubble.model.account.Account; +import bubble.model.account.HasAccountNoName; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.cobbzilla.wizard.model.IdentifiableBase; +import org.cobbzilla.wizard.model.entityconfig.annotations.*; +import org.hibernate.annotations.Type; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Transient; + +import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.ENCRYPTED_LONG; +import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.ENC_LONG; + +@ECType(root=true) @ECTypeURIs(listFields={"account", "plan", "accountPlan", "payment", "bill"}) +@ECTypeFields(list={"account", "plan", "accountPlan", "payment", "bill"}) +@ECIndexes({ + @ECIndex(unique=true, of={"planPaymentMethod", "bill"}) +}) +@Entity @NoArgsConstructor @Accessors(chain=true) +public class AccountPlanPayment extends IdentifiableBase implements HasAccountNoName { + + @ECForeignKey(entity=Account.class) + @Column(nullable=false, updatable=false, length=UUID_MAXLEN) + @Getter @Setter private String account; + + @ECForeignKey(entity=BubblePlan.class) + @Column(nullable=false, updatable=false, length=UUID_MAXLEN) + @Getter @Setter private String plan; + + @ECForeignKey(entity=AccountPlan.class) + @Column(nullable=false, updatable=false, length=UUID_MAXLEN) + @Getter @Setter private String accountPlan; + + @ECForeignKey(entity=AccountPayment.class) + @Column(nullable=false, updatable=false, length=UUID_MAXLEN) + @Getter @Setter private String payment; + + @ECForeignKey(entity=AccountPaymentMethod.class) + @Column(nullable=false, updatable=false, length=UUID_MAXLEN) + @Getter @Setter private String paymentMethod; + + @ECForeignKey(entity=AccountPlanPaymentMethod.class) + @Column(nullable=false, updatable=false, length=UUID_MAXLEN) + @Getter @Setter private String planPaymentMethod; + + @ECIndex(unique=true) @ECForeignKey(index=false, entity=Bill.class) + @Column(nullable=false, updatable=false, length=UUID_MAXLEN) + @Getter @Setter private String bill; + + @Column(nullable=false, updatable=false, length=50) + @ECIndex @Getter @Setter private String period; + + @Type(type=ENCRYPTED_LONG) @Column(updatable=false, columnDefinition="varchar("+(ENC_LONG)+") NOT NULL") + @Getter @Setter private Long amount = 0L; + + @ECIndex @Column(nullable=false, updatable=false, length=10) + @Getter @Setter private String currency; + + @Transient @Getter @Setter private transient AccountPlan accountPlanObject; + @Transient @Getter @Setter private transient AccountPayment paymentObject; + @Transient @Getter @Setter private transient Bill billObject; + +} diff --git a/bubble-server/src/main/java/bubble/model/bill/Bill.java b/bubble-server/src/main/java/bubble/model/bill/Bill.java index 9192263b..e6b2d6ad 100644 --- a/bubble-server/src/main/java/bubble/model/bill/Bill.java +++ b/bubble-server/src/main/java/bubble/model/bill/Bill.java @@ -16,8 +16,8 @@ import javax.persistence.*; import static org.cobbzilla.util.daemon.ZillaRuntime.big; import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.*; -@ECType(root=true) @ECTypeURIs(listFields={"name", "type", "quantity", "price", "period"}) -@ECTypeFields(list={"name", "type", "quantity", "price", "period"}) +@ECType(root=true) @ECTypeURIs(listFields={"name", "status", "type", "quantity", "price", "period"}) +@ECTypeFields(list={"name", "Status", "type", "quantity", "price", "period"}) @Entity @NoArgsConstructor @Accessors(chain=true) @ECIndexes({ @ECIndex(unique=true, of={"account", "plan", "type", "period"}) @@ -28,22 +28,30 @@ public class Bill extends IdentifiableBase implements HasAccountNoName { @Column(nullable=false, updatable=false, length=UUID_MAXLEN) @Getter @Setter private String account; - @ECForeignKey(entity=AccountPlan.class) + @ECForeignKey(entity=BubblePlan.class) @Column(nullable=false, updatable=false, length=UUID_MAXLEN) @Getter @Setter private String plan; + @ECForeignKey(entity=AccountPlan.class) + @Column(nullable=false, updatable=false, length=UUID_MAXLEN) + @Getter @Setter private String accountPlan; + + @ECIndex @Enumerated(EnumType.STRING) + @Column(nullable=false, length=20) + @Getter @Setter private BillStatus status = BillStatus.unpaid; + @ECIndex @Enumerated(EnumType.STRING) @Column(nullable=false, updatable=false, length=20) @Getter @Setter private BillItemType type; - @Column(nullable=false, updatable=false, length=10) + @Column(nullable=false, updatable=false, length=50) @ECIndex @Getter @Setter private String period; @Type(type=ENCRYPTED_LONG) @Column(updatable=false, columnDefinition="varchar("+(ENC_LONG)+") NOT NULL") @Getter @Setter private Long quantity = 0L; - @Type(type=ENCRYPTED_INTEGER) @Column(updatable=false, columnDefinition="varchar("+(ENC_INT)+") NOT NULL") - @Getter @Setter private Integer price = 0; + @Type(type=ENCRYPTED_LONG) @Column(updatable=false, columnDefinition="varchar("+(ENC_LONG)+") NOT NULL") + @Getter @Setter private Long price = 0L; @ECIndex @Column(nullable=false, updatable=false, length=10) @Getter @Setter private String currency; diff --git a/bubble-server/src/main/java/bubble/model/bill/BillPeriod.java b/bubble-server/src/main/java/bubble/model/bill/BillPeriod.java index 01b9be59..b9932ca4 100644 --- a/bubble-server/src/main/java/bubble/model/bill/BillPeriod.java +++ b/bubble-server/src/main/java/bubble/model/bill/BillPeriod.java @@ -1,13 +1,22 @@ package bubble.model.bill; import com.fasterxml.jackson.annotation.JsonCreator; +import lombok.AllArgsConstructor; +import org.joda.time.format.DateTimeFormatter; import static bubble.ApiConstants.enumFromString; +import static org.cobbzilla.util.daemon.ZillaRuntime.now; +import static org.cobbzilla.util.time.TimeUtil.DATE_FORMAT_YYYY_MM; +@AllArgsConstructor public enum BillPeriod { - monthly; + monthly (DATE_FORMAT_YYYY_MM); + + private DateTimeFormatter formatter; @JsonCreator public static BillPeriod fromString (String v) { return enumFromString(BillPeriod.class, v); } + public String currentPeriod() { return formatter.print(now()); } + } diff --git a/bubble-server/src/main/java/bubble/model/bill/BillStatus.java b/bubble-server/src/main/java/bubble/model/bill/BillStatus.java new file mode 100644 index 00000000..0f4730b3 --- /dev/null +++ b/bubble-server/src/main/java/bubble/model/bill/BillStatus.java @@ -0,0 +1,13 @@ +package bubble.model.bill; + +import com.fasterxml.jackson.annotation.JsonCreator; + +import static bubble.ApiConstants.enumFromString; + +public enum BillStatus { + + unpaid, paid; + + @JsonCreator public static BillStatus fromString (String v) { return enumFromString(BillStatus.class, v); } + +} diff --git a/bubble-server/src/main/java/bubble/model/bill/BubblePlan.java b/bubble-server/src/main/java/bubble/model/bill/BubblePlan.java index e75ec364..eeb47875 100644 --- a/bubble-server/src/main/java/bubble/model/bill/BubblePlan.java +++ b/bubble-server/src/main/java/bubble/model/bill/BubblePlan.java @@ -73,7 +73,7 @@ public class BubblePlan extends IdentifiableBase implements HasAccount { public boolean enabled () { return enabled == null || enabled; } @ECIndex @Column(nullable=false) - @Getter @Setter private Integer price; + @Getter @Setter private Long price; @ECIndex @Column(nullable=false, length=10) @Getter @Setter private String currency = "USD"; diff --git a/bubble-server/src/main/java/bubble/model/bill/PaymentMethodType.java b/bubble-server/src/main/java/bubble/model/bill/PaymentMethodType.java index 3996139c..f119cde7 100644 --- a/bubble-server/src/main/java/bubble/model/bill/PaymentMethodType.java +++ b/bubble-server/src/main/java/bubble/model/bill/PaymentMethodType.java @@ -6,9 +6,10 @@ import static bubble.ApiConstants.enumFromString; public enum PaymentMethodType { - credit, paypal, code, free; + credit, code, free; public boolean requiresClaim() { return this == code; } + public boolean requiresAuth() { return this == credit; } @JsonCreator public static PaymentMethodType fromString(String v) { return enumFromString(PaymentMethodType.class, v); } diff --git a/bubble-server/src/main/java/bubble/model/cloud/notify/NotificationType.java b/bubble-server/src/main/java/bubble/model/cloud/notify/NotificationType.java index af3335a0..42faf4c7 100644 --- a/bubble-server/src/main/java/bubble/model/cloud/notify/NotificationType.java +++ b/bubble-server/src/main/java/bubble/model/cloud/notify/NotificationType.java @@ -5,9 +5,10 @@ import bubble.cloud.compute.ComputeNodeSize; import bubble.cloud.geoCode.GeoCodeResult; import bubble.cloud.geoLocation.GeoLocation; import bubble.cloud.geoTime.GeoTimeZone; -import bubble.cloud.payment.PaymentValidationResult; import bubble.model.cloud.BubbleNode; import bubble.notify.ReceivedNotificationHandler; +import bubble.notify.payment.PaymentResult; +import bubble.notify.payment.PaymentValidationResult; import bubble.notify.storage.StorageResult; import bubble.server.BubbleConfiguration; import com.fasterxml.jackson.annotation.JsonCreator; @@ -89,8 +90,9 @@ public enum NotificationType { // delegated payment driver notifications payment_driver_validate (PaymentValidationResult.class), payment_driver_claim (PaymentValidationResult.class), - payment_driver_purchase (Boolean.class), - payment_driver_refund (Boolean.class), + payment_driver_authorize (PaymentResult.class), + payment_driver_purchase (PaymentResult.class), + payment_driver_refund (PaymentResult.class), payment_driver_response (true); private String packageName = "bubble.notify"; diff --git a/bubble-server/src/main/java/bubble/notify/NewNodeNotification.java b/bubble-server/src/main/java/bubble/notify/NewNodeNotification.java index 49efbcc7..7fe458a7 100644 --- a/bubble-server/src/main/java/bubble/notify/NewNodeNotification.java +++ b/bubble-server/src/main/java/bubble/notify/NewNodeNotification.java @@ -10,6 +10,7 @@ import static org.cobbzilla.util.daemon.ZillaRuntime.empty; @NoArgsConstructor @Accessors(chain=true) public class NewNodeNotification { + @Getter @Setter private String account; @Getter @Setter private String host; @Getter @Setter private String network; @Getter @Setter private String domain; diff --git a/bubble-server/src/main/java/bubble/notify/NotificationHandler_hello_from_sage.java b/bubble-server/src/main/java/bubble/notify/NotificationHandler_hello_from_sage.java index 973a2de7..86b8659b 100644 --- a/bubble-server/src/main/java/bubble/notify/NotificationHandler_hello_from_sage.java +++ b/bubble-server/src/main/java/bubble/notify/NotificationHandler_hello_from_sage.java @@ -107,6 +107,7 @@ public class NotificationHandler_hello_from_sage extends ReceivedNotificationHan final CloudService cloud = closestNotUs.getCloud(); final NewNodeNotification newNodeRequest = new NewNodeNotification() + .setAccount(network.getAccount()) .setNetwork(network.getUuid()) .setDomain(network.getDomain()) .setCloud(cloud.getUuid()) diff --git a/bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver.java b/bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver.java index dfac8b18..3d37d3f5 100644 --- a/bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver.java +++ b/bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver.java @@ -1,9 +1,7 @@ package bubble.notify.payment; -import bubble.dao.bill.AccountPaymentMethodDAO; import bubble.dao.cloud.BubbleNodeDAO; import bubble.dao.cloud.CloudServiceDAO; -import bubble.model.bill.AccountPaymentMethod; import bubble.model.cloud.BubbleNode; import bubble.model.cloud.CloudService; import bubble.model.cloud.notify.ReceivedNotification; @@ -17,7 +15,6 @@ import static org.cobbzilla.util.json.JsonUtil.json; public abstract class NotificationHandler_payment_driver extends DelegatedNotificationHandlerBase { @Autowired protected BubbleNodeDAO nodeDAO; - @Autowired protected AccountPaymentMethodDAO paymentMethodDAO; @Autowired protected CloudServiceDAO cloudDAO; @Override public void handleNotification(ReceivedNotification n) { @@ -25,12 +22,18 @@ public abstract class NotificationHandler_payment_driver extends DelegatedNotifi if (sender == null) die("sender not found: "+n.getFromNode()); final PaymentNotification paymentNotification = json(n.getPayloadJson(), PaymentNotification.class); - final AccountPaymentMethod paymentMethod = paymentMethodDAO.findByUuid(paymentNotification.getPaymentMethodUuid()); - - final CloudService paymentService = cloudDAO.findByAccountAndName(configuration.getThisNode().getAccount(), paymentMethod.getCloud()); - final boolean success = handlePaymentRequest(paymentNotification, paymentService); - - notifySender(payment_driver_response, n.getNotificationId(), sender, success); + final CloudService paymentService = cloudDAO.findByAccountAndName(configuration.getThisNode().getAccount(), paymentNotification.getCloud()); + PaymentResult result; + try { + if (handlePaymentRequest(paymentNotification, paymentService)) { + result = PaymentResult.SUCCESS; + } else { + result = PaymentResult.FAILURE; + } + } catch (Exception e) { + result = PaymentResult.exception(e); + } + notifySender(payment_driver_response, n.getNotificationId(), sender, result); } protected abstract boolean handlePaymentRequest(PaymentNotification paymentNotification, diff --git a/bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_authorize.java b/bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_authorize.java new file mode 100644 index 00000000..19eb96ca --- /dev/null +++ b/bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_authorize.java @@ -0,0 +1,26 @@ +package bubble.notify.payment; + +import bubble.dao.bill.AccountPaymentMethodDAO; +import bubble.dao.bill.AccountPlanDAO; +import bubble.dao.bill.BubblePlanDAO; +import bubble.model.bill.AccountPaymentMethod; +import bubble.model.bill.AccountPlan; +import bubble.model.bill.BubblePlan; +import bubble.model.cloud.CloudService; +import org.springframework.beans.factory.annotation.Autowired; + +public class NotificationHandler_payment_driver_authorize extends NotificationHandler_payment_driver { + + @Autowired private BubblePlanDAO planDAO; + @Autowired private AccountPaymentMethodDAO paymentMethodDAO; + @Autowired private AccountPlanDAO accountPlanDAO; + + @Override public boolean handlePaymentRequest(PaymentNotification paymentNotification, CloudService paymentService) { + // todo: this returns null because the AccountPlan has not yet been created.... + final AccountPlan accountPlan = accountPlanDAO.findByUuid(paymentNotification.getAccountPlanUuid()); + final BubblePlan plan = planDAO.findByUuid(accountPlan.getPlan()); + final AccountPaymentMethod paymentMethod = paymentMethodDAO.findByUuid(paymentNotification.getPaymentMethodUuid()); + return paymentService.getPaymentDriver(configuration).authorize(plan, accountPlan, paymentMethod); + } + +} diff --git a/bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_claim.java b/bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_claim.java index 87be8b49..3048849f 100644 --- a/bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_claim.java +++ b/bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_claim.java @@ -1,7 +1,6 @@ package bubble.notify.payment; import bubble.cloud.payment.PaymentServiceDriver; -import bubble.cloud.payment.PaymentValidationResult; import bubble.dao.cloud.BubbleNodeDAO; import bubble.dao.cloud.CloudServiceDAO; import bubble.model.cloud.BubbleNode; diff --git a/bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_purchase.java b/bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_purchase.java index 5b3a7746..85b7807b 100644 --- a/bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_purchase.java +++ b/bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_purchase.java @@ -8,9 +8,7 @@ public class NotificationHandler_payment_driver_purchase extends NotificationHan return paymentService.getPaymentDriver(configuration).purchase( paymentNotification.getAccountPlanUuid(), paymentNotification.getPaymentMethodUuid(), - paymentNotification.getBillUuid(), - paymentNotification.getPurchaseAmount(), - paymentNotification.getCurrency() + paymentNotification.getBillUuid() ); } diff --git a/bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_refund.java b/bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_refund.java index 90919741..c5ebee00 100644 --- a/bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_refund.java +++ b/bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_refund.java @@ -9,8 +9,7 @@ public class NotificationHandler_payment_driver_refund extends NotificationHandl paymentNotification.getAccountPlanUuid(), paymentNotification.getPaymentMethodUuid(), paymentNotification.getBillUuid(), - paymentNotification.getPurchaseAmount(), - paymentNotification.getCurrency() + paymentNotification.getAmount() ); } diff --git a/bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_validate.java b/bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_validate.java index 10b45179..58f4a306 100644 --- a/bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_validate.java +++ b/bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_validate.java @@ -1,6 +1,5 @@ package bubble.notify.payment; -import bubble.cloud.payment.PaymentValidationResult; import bubble.dao.cloud.BubbleNodeDAO; import bubble.dao.cloud.CloudServiceDAO; import bubble.model.cloud.BubbleNode; diff --git a/bubble-server/src/main/java/bubble/notify/payment/PaymentMethodClaimNotification.java b/bubble-server/src/main/java/bubble/notify/payment/PaymentMethodClaimNotification.java index 746bc097..5aa08d2c 100644 --- a/bubble-server/src/main/java/bubble/notify/payment/PaymentMethodClaimNotification.java +++ b/bubble-server/src/main/java/bubble/notify/payment/PaymentMethodClaimNotification.java @@ -19,12 +19,12 @@ public class PaymentMethodClaimNotification extends SynchronousNotification { @Getter @Setter private String cloud; - public PaymentMethodClaimNotification(AccountPaymentMethod paymentMethod, String cloud) { + public PaymentMethodClaimNotification(String cloud, AccountPaymentMethod paymentMethod) { this.paymentMethod = paymentMethod; this.cloud = cloud; } - public PaymentMethodClaimNotification(AccountPlanPaymentMethod planPaymentMethod, String cloud) { + public PaymentMethodClaimNotification(String cloud, AccountPlanPaymentMethod planPaymentMethod) { this.planPaymentMethod = planPaymentMethod; this.cloud = cloud; } diff --git a/bubble-server/src/main/java/bubble/notify/payment/PaymentMethodValidationNotification.java b/bubble-server/src/main/java/bubble/notify/payment/PaymentMethodValidationNotification.java index d0352077..534d1abf 100644 --- a/bubble-server/src/main/java/bubble/notify/payment/PaymentMethodValidationNotification.java +++ b/bubble-server/src/main/java/bubble/notify/payment/PaymentMethodValidationNotification.java @@ -11,7 +11,7 @@ import lombok.experimental.Accessors; @NoArgsConstructor @AllArgsConstructor @Accessors(chain=true) public class PaymentMethodValidationNotification extends SynchronousNotification { - @Getter @Setter private AccountPaymentMethod paymentMethod; @Getter @Setter private String cloud; + @Getter @Setter private AccountPaymentMethod paymentMethod; } diff --git a/bubble-server/src/main/java/bubble/notify/payment/PaymentNotification.java b/bubble-server/src/main/java/bubble/notify/payment/PaymentNotification.java index b88cb1fb..e36f3001 100644 --- a/bubble-server/src/main/java/bubble/notify/payment/PaymentNotification.java +++ b/bubble-server/src/main/java/bubble/notify/payment/PaymentNotification.java @@ -1,19 +1,18 @@ package bubble.notify.payment; import bubble.notify.SynchronousNotification; -import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.experimental.Accessors; -@NoArgsConstructor @AllArgsConstructor @Accessors(chain=true) +@NoArgsConstructor @Accessors(chain=true) public class PaymentNotification extends SynchronousNotification { + @Getter @Setter private String cloud; @Getter @Setter private String accountPlanUuid; @Getter @Setter private String paymentMethodUuid; @Getter @Setter private String billUuid; - @Getter @Setter private int purchaseAmount; - @Getter @Setter private String currency; + @Getter @Setter private long amount; } diff --git a/bubble-server/src/main/java/bubble/notify/payment/PaymentResult.java b/bubble-server/src/main/java/bubble/notify/payment/PaymentResult.java new file mode 100644 index 00000000..2dd49524 --- /dev/null +++ b/bubble-server/src/main/java/bubble/notify/payment/PaymentResult.java @@ -0,0 +1,50 @@ +package bubble.notify.payment; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.cobbzilla.wizard.validation.ConstraintViolationBean; +import org.cobbzilla.wizard.validation.MultiViolationException; +import org.cobbzilla.wizard.validation.SimpleViolationException; + +import java.util.Arrays; +import java.util.List; + +import static org.cobbzilla.util.daemon.ZillaRuntime.empty; +import static org.cobbzilla.util.daemon.ZillaRuntime.errorString; + +@NoArgsConstructor @Accessors(chain=true) +public class PaymentResult { + + public static final PaymentResult SUCCESS = new PaymentResult().setSuccess(true); + public static final PaymentResult FAILURE = new PaymentResult().setSuccess(false); + + @Getter @Setter private Boolean success; + public boolean success() { return success != null && success; } + + @Getter @Setter private ConstraintViolationBean[] violations; + public boolean hasViolations() { return !empty(violations); } + public List 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)); + } + } + +} diff --git a/bubble-server/src/main/java/bubble/cloud/payment/PaymentValidationResult.java b/bubble-server/src/main/java/bubble/notify/payment/PaymentValidationResult.java similarity index 92% rename from bubble-server/src/main/java/bubble/cloud/payment/PaymentValidationResult.java rename to bubble-server/src/main/java/bubble/notify/payment/PaymentValidationResult.java index 1b982c92..b181b5e3 100644 --- a/bubble-server/src/main/java/bubble/cloud/payment/PaymentValidationResult.java +++ b/bubble-server/src/main/java/bubble/notify/payment/PaymentValidationResult.java @@ -1,4 +1,4 @@ -package bubble.cloud.payment; +package bubble.notify.payment; import bubble.model.bill.AccountPaymentMethod; import lombok.Getter; @@ -19,6 +19,7 @@ public class PaymentValidationResult { @Getter @Setter private AccountPaymentMethod paymentMethod; @Getter @Setter private ConstraintViolationBean[] violations; + public boolean hasViolations() { return !empty(violations); } public List violationsList() { return Arrays.asList(violations); } public PaymentValidationResult(AccountPaymentMethod paymentMethod) { this.paymentMethod = paymentMethod; } diff --git a/bubble-server/src/main/java/bubble/resources/account/ReadOnlyAccountOwnedResource.java b/bubble-server/src/main/java/bubble/resources/account/ReadOnlyAccountOwnedResource.java new file mode 100644 index 00000000..5f6b8f91 --- /dev/null +++ b/bubble-server/src/main/java/bubble/resources/account/ReadOnlyAccountOwnedResource.java @@ -0,0 +1,18 @@ +package bubble.resources.account; + +import bubble.dao.account.AccountOwnedEntityDAO; +import bubble.model.account.Account; +import bubble.model.account.HasAccount; +import org.glassfish.grizzly.http.server.Request; +import org.glassfish.jersey.server.ContainerRequest; + +public class ReadOnlyAccountOwnedResource> + extends AccountOwnedResource { + + 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; } + +} diff --git a/bubble-server/src/main/java/bubble/resources/bill/AccountPaymentsResource.java b/bubble-server/src/main/java/bubble/resources/bill/AccountPaymentsResource.java index f382e1ec..96d43f2c 100644 --- a/bubble-server/src/main/java/bubble/resources/bill/AccountPaymentsResource.java +++ b/bubble-server/src/main/java/bubble/resources/bill/AccountPaymentsResource.java @@ -3,55 +3,19 @@ package bubble.resources.bill; import bubble.dao.bill.AccountPaymentDAO; import bubble.model.account.Account; import bubble.model.bill.AccountPayment; -import bubble.model.bill.AccountPlan; -import bubble.model.bill.Bill; -import bubble.resources.account.AccountOwnedResource; +import bubble.resources.account.ReadOnlyAccountOwnedResource; import lombok.extern.slf4j.Slf4j; -import org.glassfish.grizzly.http.server.Request; -import org.glassfish.jersey.server.ContainerRequest; import javax.ws.rs.Consumes; import javax.ws.rs.Produces; -import java.util.List; import static org.cobbzilla.util.http.HttpContentTypes.APPLICATION_JSON; @Consumes(APPLICATION_JSON) @Produces(APPLICATION_JSON) @Slf4j -public class AccountPaymentsResource extends AccountOwnedResource { - - private AccountPlan accountPlan; - private Bill bill; +public class AccountPaymentsResource extends ReadOnlyAccountOwnedResource { 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 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()); - } - } diff --git a/bubble-server/src/main/java/bubble/resources/bill/AccountPlanPaymentsResource.java b/bubble-server/src/main/java/bubble/resources/bill/AccountPlanPaymentsResource.java new file mode 100644 index 00000000..059e8a1e --- /dev/null +++ b/bubble-server/src/main/java/bubble/resources/bill/AccountPlanPaymentsResource.java @@ -0,0 +1,96 @@ +package bubble.resources.bill; + +import bubble.dao.bill.AccountPaymentDAO; +import bubble.dao.bill.AccountPlanDAO; +import bubble.dao.bill.AccountPlanPaymentDAO; +import bubble.dao.bill.BillDAO; +import bubble.model.account.Account; +import bubble.model.bill.AccountPayment; +import bubble.model.bill.AccountPlan; +import bubble.model.bill.AccountPlanPayment; +import bubble.model.bill.Bill; +import bubble.resources.account.ReadOnlyAccountOwnedResource; +import lombok.extern.slf4j.Slf4j; +import org.glassfish.jersey.server.ContainerRequest; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import java.util.List; + +import static bubble.ApiConstants.EP_BILL; +import static bubble.ApiConstants.EP_PAYMENT; +import static org.cobbzilla.wizard.resources.ResourceUtil.*; + +@Slf4j +public class AccountPlanPaymentsResource extends ReadOnlyAccountOwnedResource { + + @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 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); + } + +} diff --git a/bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java b/bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java index e436da30..79516155 100644 --- a/bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java +++ b/bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java @@ -62,11 +62,11 @@ public class AccountPlansResource extends AccountOwnedResource { +public class BillsResource extends ReadOnlyAccountOwnedResource { private AccountPlan accountPlan; @@ -29,26 +28,22 @@ public class BillsResource extends AccountOwnedResource { 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 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); } } diff --git a/bubble-server/src/main/java/bubble/resources/cloud/NodesResource.java b/bubble-server/src/main/java/bubble/resources/cloud/NodesResource.java index 85d74931..0e629d86 100644 --- a/bubble-server/src/main/java/bubble/resources/cloud/NodesResource.java +++ b/bubble-server/src/main/java/bubble/resources/cloud/NodesResource.java @@ -5,9 +5,8 @@ import bubble.model.account.Account; import bubble.model.cloud.BubbleDomain; import bubble.model.cloud.BubbleNetwork; import bubble.model.cloud.BubbleNode; -import bubble.resources.account.AccountOwnedResource; +import bubble.resources.account.ReadOnlyAccountOwnedResource; import lombok.extern.slf4j.Slf4j; -import org.glassfish.grizzly.http.server.Request; import org.glassfish.jersey.server.ContainerRequest; import java.util.List; @@ -15,7 +14,7 @@ import java.util.List; import static org.cobbzilla.wizard.resources.ResourceUtil.forbiddenEx; @Slf4j -public class NodesResource extends AccountOwnedResource { +public class NodesResource extends ReadOnlyAccountOwnedResource { private BubbleNetwork network; private BubbleDomain domain; @@ -60,21 +59,6 @@ public class NodesResource extends AccountOwnedResource 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 lock = new AtomicReference<>(existingLock); daemon(new NodeLauncher(newNodeRequest, lock, this)); } diff --git a/bubble-server/src/main/resources/message_templates/server/en_US/post_auth/ResourceMessages.properties b/bubble-server/src/main/resources/message_templates/server/en_US/post_auth/ResourceMessages.properties index ee578acd..b220ae90 100644 --- a/bubble-server/src/main/resources/message_templates/server/en_US/post_auth/ResourceMessages.properties +++ b/bubble-server/src/main/resources/message_templates/server/en_US/post_auth/ResourceMessages.properties @@ -71,6 +71,8 @@ payment_description_free=Enjoy Bubble for FREE! # Error messages from API server err.accountContactsJson.length=Account contacts length violation err.accountPlan.callerCountryDisallowed=Your country is not currently supported by our platform +err.accountPlan.disabled=Account plan is not enabled +err.accountPlan.notFound=Account plan not found err.admin.cannotRemoveAdminStatusFromSelf=You cannot remove admin status from your own account err.allowedCountriesJson.length=Allowed countries list is too long err.approval.invalid=Approval cannot proceed @@ -179,6 +181,7 @@ err.privateKeyHash.length=Private key hash is too long err.privateKey.length=Private key is too long err.purchase.accountMismatch=Invalid payment err.purchase.amountMismatch=Payment amount does not match billed amount +err.purchase.authNotFound=Payment authorization was not found err.purchase.billNotFound=Bill not found err.purchase.cardError=Error processing credit card payment err.purchase.cardProcessingError=Error processing credit card payment @@ -195,6 +198,7 @@ err.purchase.tokenExpired=Invite code has expired err.purchase.tokenUsed=Invite code has already been used err.purchase.tokenInvalid=Error reading invite code err.purchase.tokenMismatch=Error reading invite code +err.purchase.underReview=Charge is currently under review err.purchase.declined=Purchase was unsuccessful err.quantity.length=Quantity is too long err.region.required=Region is required diff --git a/bubble-server/src/test/java/bubble/test/PaymentTest.java b/bubble-server/src/test/java/bubble/test/PaymentTest.java index 0ebef232..086571db 100644 --- a/bubble-server/src/test/java/bubble/test/PaymentTest.java +++ b/bubble-server/src/test/java/bubble/test/PaymentTest.java @@ -2,14 +2,9 @@ package bubble.test; import bubble.server.BubbleConfiguration; import lombok.extern.slf4j.Slf4j; -import org.cobbzilla.util.http.PhantomUtil; import org.cobbzilla.wizard.server.RestServer; import org.junit.Test; -import java.util.concurrent.TimeUnit; - -import static org.cobbzilla.util.system.Sleep.sleep; - @Slf4j public class PaymentTest extends ActivatedBubbleModelTestBase { @@ -27,11 +22,9 @@ public class PaymentTest extends ActivatedBubbleModelTestBase { @Test public void testCodePayment () throws Exception { modelTest("payment/pay_code"); } @Test public void testCreditPayment () throws Exception { modelTest("payment/pay_credit"); } - @Test public void testPhantom () throws Exception { - log.info("go for it!"); - sleep(TimeUnit.MINUTES.toMillis(60)); - PhantomUtil.init(); - // new PhantomUtil().loadPageAndExec(getConfiguration().getPublicUriBase()+"/stripe/index.html", ""); + @Test public void testCreditPaymentMultipleStartStop () throws Exception { + modelTest("payment/pay_credit_multi_start_stop"); } + } diff --git a/bubble-server/src/test/resources/models/tests/payment/pay_code.json b/bubble-server/src/test/resources/models/tests/payment/pay_code.json index 68d3f30f..ecc5cc86 100644 --- a/bubble-server/src/test/resources/models/tests/payment/pay_code.json +++ b/bubble-server/src/test/resources/models/tests/payment/pay_code.json @@ -30,6 +30,15 @@ } }, + { + "comment": "get plans", + "request": { "uri": "plans" }, + "response": { + "store": "plans", + "check": [{"condition": "json.length >= 1"}] + } + }, + { "comment": "add plan, using 'code' payment method but with an invalid code", "request": { @@ -40,7 +49,7 @@ "domain": "{{defaultDomain}}", "locale": "en_US", "timezone": "EST", - "plan": "bubble", + "plan": "{{plans.[0].name}}", "footprint": "US", "paymentMethod": { "paymentMethodType": "code", @@ -66,7 +75,7 @@ "domain": "{{defaultDomain}}", "locale": "en_US", "timezone": "EST", - "plan": "bubble", + "plan": "{{plans.[0].name}}", "footprint": "US", "paymentMethod": { "paymentMethodType": "code", @@ -122,7 +131,7 @@ "domain": "{{defaultDomain}}", "locale": "en_US", "timezone": "EST", - "plan": "bubble", + "plan": "{{plans.[0].name}}", "footprint": "US", "paymentMethod": { "paymentMethodType": "code", diff --git a/bubble-server/src/test/resources/models/tests/payment/pay_credit.json b/bubble-server/src/test/resources/models/tests/payment/pay_credit.json index f2fa2952..5776771e 100644 --- a/bubble-server/src/test/resources/models/tests/payment/pay_credit.json +++ b/bubble-server/src/test/resources/models/tests/payment/pay_credit.json @@ -31,7 +31,16 @@ }, { - "comment": "get payment methods, so we can get the stripe public key to tokenize a credit card", + "comment": "get plans", + "request": { "uri": "plans" }, + "response": { + "store": "plans", + "check": [{"condition": "json.length >= 1"}] + } + }, + + { + "comment": "get payment methods, tokenize a credit card", "request": { "uri": "paymentMethods" }, "response": { "store": "paymentMethods" @@ -49,7 +58,7 @@ "domain": "{{defaultDomain}}", "locale": "en_US", "timezone": "EST", - "plan": "bubble", + "plan": "{{plans.[0].name}}", "footprint": "US", "paymentMethod": { "paymentMethodType": "credit", @@ -75,7 +84,7 @@ "domain": "{{defaultDomain}}", "locale": "en_US", "timezone": "EST", - "plan": "bubble", + "plan": "{{plans.[0].name}}", "footprint": "US", "paymentMethod": { "paymentMethodType": "credit", @@ -127,7 +136,7 @@ "domain": "{{defaultDomain}}", "locale": "en_US", "timezone": "EST", - "plan": "bubble", + "plan": "{{plans.[0].name}}", "footprint": "US", "paymentMethod": { "paymentMethodType": "credit", @@ -152,12 +161,14 @@ }, { + "before": "sleep 5s", "comment": "verify account plans, should be one", "request": { "uri": "me/plans" }, "response": { "check": [ {"condition": "json.length == 1"}, - {"condition": "json[0].getName() == accountPlan.getName()"} + {"condition": "json[0].getName() == accountPlan.getName()"}, + {"condition": "json[0].enabled()"} ] } }, diff --git a/bubble-server/src/test/resources/models/tests/payment/pay_credit_multi_start_stop.json b/bubble-server/src/test/resources/models/tests/payment/pay_credit_multi_start_stop.json new file mode 100644 index 00000000..0c3b0e7f --- /dev/null +++ b/bubble-server/src/test/resources/models/tests/payment/pay_credit_multi_start_stop.json @@ -0,0 +1,332 @@ +[ + { + "comment": "create a user account", + "request": { + "uri": "users", + "method": "put", + "entity": { + "name": "test_user", + "password": "password", + "contact": {"type": "email", "info": "test-user@example.com"} + } + } + }, + + { + "before": "sleep 22s", // wait for account objects to be created + "comment": "login as new user", + "request": { + "session": "new", + "uri": "auth/login", + "entity": { + "name": "test_user", + "password": "password" + } + }, + "response": { + "store": "testAccount", + "sessionName": "userSession", + "session": "token" + } + }, + + { + "comment": "get payment methods, tokenize a credit card", + "request": { "uri": "paymentMethods" }, + "response": { + "store": "paymentMethods" + }, + "after": "stripe_tokenize_card" + }, + + { + "before": "sleep 1s", + "comment": "as root, check email inbox for verification message", + "request": { + "session": "rootSession", + "uri": "debug/inbox/email/test-user@example.com?type=request&action=verify&target=account" + }, + "response": { + "store": "emailInbox", + "check": [ + {"condition": "'{{json.[0].ctx.message.messageType}}' === 'request'"}, + {"condition": "'{{json.[0].ctx.message.action}}' === 'verify'"}, + {"condition": "'{{json.[0].ctx.message.target}}' === 'account'"} + ] + } + }, + + { + "comment": "approve email verification request", + "request": { + "session": "userSession", + "uri": "auth/approve/{{emailInbox.[0].ctx.confirmationToken}}", + "method": "post" + } + }, + + { + "comment": "get plans", + "request": { "uri": "plans" }, + "response": { + "store": "plans", + "check": [{"condition": "json.length >= 1"}] + } + }, + + { + "comment": "add plan, using 'credit' payment method with a valid card token, creates a stripe customer", + "request": { + "uri": "me/plans", + "method": "put", + "entity": { + "name": "test-net-{{rand 5}}", + "domain": "{{defaultDomain}}", + "locale": "en_US", + "timezone": "EST", + "plan": "{{plans.[0].name}}", + "footprint": "US", + "paymentMethod": { + "paymentMethodType": "credit", + "paymentInfo": "{{stripeToken}}" + } + } + }, + "response": { + "store": "accountPlan" + } + }, + + { + "comment": "verify account payment methods, should be one", + "request": { "uri": "me/paymentMethods" }, + "response": { + "check": [ + {"condition": "json.length === 1"}, + {"condition": "json[0].getPaymentMethodType().name() === 'credit'"} + ] + } + }, + + { + "comment": "verify account plans, should be one", + "request": { "uri": "me/plans" }, + "response": { + "check": [ + {"condition": "json.length === 1"}, + {"condition": "json[0].getName() === accountPlan.getName()"} + ] + } + }, + + { + "comment": "verify account plan payment info", + "request": { "uri": "me/plans/{{accountPlan.uuid}}/paymentMethod" }, + "response": { + "store": "savedPaymentMethods", + "check": [ + {"condition": "json.getPaymentMethodType().name() === 'credit'"}, + {"condition": "json.getMaskedPaymentInfo() === 'XXXX-XXXX-XXXX-4242'"} + ] + } + }, + + { + "comment": "verify bill exists for new service with correct price and has been paid", + "request": { "uri": "me/bills" }, + "response": { + "store": "bills", + "check": [ + {"condition": "json.length === 1"}, + {"condition": "json[0].getPlan() === '{{accountPlan.uuid}}'"}, + {"condition": "json[0].getQuantity() === 1"}, + {"condition": "json[0].getPrice() === {{plans.[0].price}}"}, + {"condition": "json[0].getTotal() === {{plans.[0].price}}"}, + {"condition": "json[0].getStatus().name() === 'paid'"} + ] + } + }, + + { + "comment": "verify successful payment exists for new service", + "request": { "uri": "me/payments" }, + "response": { + "store": "payments", + "check": [ + {"condition": "json.length === 1"}, + {"condition": "json[0].getPlan() === '{{accountPlan.uuid}}'"}, + {"condition": "json[0].getAmount() === '{{plans.[0].price}}'"}, + {"condition": "json[0].getStatus().name() === 'success'"} + ] + } + }, + + { + "comment": "verify successful payment has paid for the bill above", + "request": { "uri": "me/payments/{{payments.[0].uuid}}/bills" }, + "response": { + "check": [ + {"condition": "json.length === 1"}, + {"condition": "json[0].getUuid() === '{{bills.[0].uuid}}'"} + ] + } + }, + + { + "comment": "delete plan", + "request": { + "method": "delete", + "uri": "me/plans/{{accountPlan.uuid}}" + } + }, + + { + "comment": "verify no plans exist", + "request": { "uri": "me/plans" }, + "response": { + "check": [ {"condition": "json.length === 0"} ] + } + }, + + { + "comment": "add a second plan, using saved payment method", + "request": { + "uri": "me/plans", + "method": "put", + "entity": { + "name": "test-net2-{{rand 5}}", + "domain": "{{defaultDomain}}", + "locale": "en_US", + "timezone": "EST", + "plan": "{{plans.[0].name}}", + "footprint": "US", + "paymentMethod": { + "paymentMethodType": "credit", + "uuid": "{{savedPaymentMethods.[0].uuid}}" + } + } + }, + "response": { + "store": "accountPlan2" + } + }, + + { + "comment": "verify we still have only one payment method", + "request": { "uri": "me/paymentMethods" }, + "response": { + "check": [ {"condition": "json.length === 1"} ] + } + }, + + { + "comment": "verify account plan payment info is same as used before", + "request": { "uri": "me/plans/{{accountPlan.uuid}}/paymentMethod" }, + "response": { + "check": [ + {"condition": "json.getPaymentMethodType().name() === 'credit'"}, + {"condition": "json.getMaskedPaymentInfo() === 'XXXX-XXXX-XXXX-4242'"}, + {"condition": "json.getUuid() === '{{savedPaymentMethods.[0].uuid}}'"} + ] + } + }, + + { + "comment": "verify we now have two bills", + "request": { "uri": "me/bills" }, + "response": { + "check": [ + {"condition": "json.length === 2"} + ] + } + }, + + { + "comment": "verify bill exists for new service, but price is zero", + "request": { "uri": "me/plans/{{accountPlan2.uuid}}/bills" }, + "response": { + "store": "plan2bills", + "check": [ + {"condition": "json.length === 1"}, + {"condition": "json[0].getPlan() === '{{accountPlan2.uuid}}'"}, + {"condition": "json[0].getQuantity() === 1"}, + {"condition": "json[0].getPrice() === 0"}, + {"condition": "json[0].getTotal() === 0"} + ] + } + }, + + { + "comment": "verify we have made two successful payments (but one of them will be zero)", + "request": { "uri": "me/payments" }, + "response": { + "check": [ + {"condition": "json.length === 2"}, + {"condition": "json[0].getStatus().name() === 'success'"}, + {"condition": "json[1].getStatus().name() === 'success'"}, + {"condition": "_find(json, function (p) { if (p.getAmount() === {{plans.[0].price}}) return p; }) != null"}, + {"condition": "_find(json, function (p) { if (p.getAmount() === 0) return p; }) != null"} + ] + } + }, + + { + "comment": "find payments made on second plan", + "request": { "uri": "me/plans/{{accountPlan2.uuid}}/payments" }, + "response": { + "store": "plan2payments", + "check": [ + {"condition": "json.length === 1"} + ] + } + }, + + { + "comment": "verify successful payment has paid for the second bill (with zero total)", + "request": { "uri": "me/plans/{{accountPlan2.uuid}}/payments/{{plan2payments.[0].uuid}}/bills" }, + "response": { + "check": [ + {"condition": "json.length === 1"}, + {"condition": "json[0].getPlan() === '{{accountPlan2.uuid}}'"}, + {"condition": "json[0].getQuantity() === 1"}, + {"condition": "json[0].getPrice() === 0"}, + {"condition": "json[0].getTotal() === 0"} + ] + } + } + + // todo: add a third plan, this one will require a second payment since two plans are now active + + // todo: remove third plan. now only one plan is active + + // todo: fast-forward 32 days, trigger BillGenerator + + // todo: verify a new Bill exists for accountPlan2, and payment has been made successfully + + // todo: set mock such that charging the card fails + + // todo: fast-forward 32 days, trigger BillGenerator + + // todo: verify a new Bill exists for accountPlan2 and Bill remains 'unpaid', and AccountPayment failed + + // todo: verify payment reminder messages have been sent + + // todo: fast-forward 1 day, trigger BillGenerator + + // todo: verify a new Bill exists for accountPlan2 and Bill remains 'unpaid', and AccountPayment failed + + // todo: verify payment reminder messages have been sent + + // todo: fast-forward 3 days, trigger BillGenerator. + + // todo: verify a new Bill exists for accountPlan2 and Bill remains 'unpaid', and AccountPayment failed + + // todo: verify network associated with plan has been stopped + + // todo: try to start network, fails due to non-payment + + // todo: submit payment + + // todo: start network, succeeds + +] \ No newline at end of file diff --git a/bubble-server/src/test/resources/models/tests/payment/pay_free.json b/bubble-server/src/test/resources/models/tests/payment/pay_free.json index 3c8ed0a2..cd8d4b75 100644 --- a/bubble-server/src/test/resources/models/tests/payment/pay_free.json +++ b/bubble-server/src/test/resources/models/tests/payment/pay_free.json @@ -30,6 +30,15 @@ } }, + { + "comment": "get plans", + "request": { "uri": "plans" }, + "response": { + "store": "plans", + "check": [{"condition": "json.length >= 1"}] + } + }, + { "comment": "add plan, fails because we have not supplied payment information", "request": { @@ -40,7 +49,7 @@ "domain": "{{defaultDomain}}", "locale": "en_US", "timezone": "EST", - "plan": "bubble", + "plan": "{{plans.[0].name}}", "footprint": "US" } }, @@ -62,7 +71,7 @@ "domain": "{{defaultDomain}}", "locale": "en_US", "timezone": "EST", - "plan": "bubble", + "plan": "{{plans.[0].name}}", "footprint": "US", "paymentMethod": { "paymentInfo": "free" @@ -87,7 +96,7 @@ "domain": "{{defaultDomain}}", "locale": "en_US", "timezone": "EST", - "plan": "bubble", + "plan": "{{plans.[0].name}}", "footprint": "US", "paymentMethod": { "paymentMethodType": "free" @@ -112,7 +121,7 @@ "domain": "{{defaultDomain}}", "locale": "en_US", "timezone": "EST", - "plan": "bubble", + "plan": "{{plans.[0].name}}", "footprint": "US", "paymentMethod": { "paymentMethodType": "free", @@ -130,19 +139,21 @@ "request": { "uri": "me/paymentMethods" }, "response": { "check": [ - {"condition": "json.length == 1"}, - {"condition": "json[0].getPaymentMethodType().name() == 'free'"} + {"condition": "json.length === 1"}, + {"condition": "json[0].getPaymentMethodType().name() === 'free'"} ] } }, { - "comment": "verify account plans, should be one", + "before": "sleep 15s", + "comment": "verify account plans, should be one, verify enabled", "request": { "uri": "me/plans" }, "response": { "check": [ - {"condition": "json.length == 1"}, - {"condition": "json[0].getName() == accountPlan.getName()"} + {"condition": "json.length === 1"}, + {"condition": "json[0].getName() === accountPlan.getName()"}, + {"condition": "json[0].enabled()"} ] } }, @@ -152,8 +163,74 @@ "request": { "uri": "me/plans/{{accountPlan.uuid}}/paymentMethod" }, "response": { "check": [ - {"condition": "json.getPaymentMethodType().name() == 'free'"}, - {"condition": "json.getMaskedPaymentInfo() == 'XXXXXXXX'"} + {"condition": "json.getPaymentMethodType().name() === 'free'"}, + {"condition": "json.getMaskedPaymentInfo() === 'XXXXXXXX'"} + ] + } + }, + + { + "comment": "verify bill exists and is paid", + "request": { "uri": "me/bills" }, + "response": { + "check": [ + {"condition": "json.length === 1"}, + {"condition": "json[0].getPlan() === '{{plans.[0].uuid}}'"}, + {"condition": "json[0].getAccountPlan() === '{{accountPlan.uuid}}'"}, + {"condition": "json[0].getQuantity() === 1"}, + {"condition": "json[0].getPrice() === {{plans.[0].price}}"}, + {"condition": "json[0].getTotal() === {{plans.[0].price}}"}, + {"condition": "json[0].getStatus().name() === 'paid'"} + ] + } + }, + + { + "comment": "verify bill exists via plan and is paid", + "request": { "uri": "me/plans/{{accountPlan.uuid}}/bills" }, + "response": { + "check": [ + {"condition": "json.length === 1"}, + {"condition": "json[0].getPlan() === '{{plans.[0].uuid}}'"}, + {"condition": "json[0].getAccountPlan() === '{{accountPlan.uuid}}'"}, + {"condition": "json[0].getQuantity() === 1"}, + {"condition": "json[0].getPrice() === {{plans.[0].price}}"}, + {"condition": "json[0].getTotal() === {{plans.[0].price}}"}, + {"condition": "json[0].getStatus().name() === 'paid'"} + ] + } + }, + + { + "comment": "verify payment exists and is successful", + "request": { "uri": "me/payments" }, + "response": { + "check": [ + {"condition": "json.length === 1"}, + {"condition": "json[0].getPlan() === '{{plans.[0].uuid}}'"}, + {"condition": "json[0].getAccountPlan() === '{{accountPlan.uuid}}'"}, + {"condition": "json[0].getAmount() === {{plans.[0].price}}"}, + {"condition": "json[0].getStatus().name() === 'success'"} + ] + } + }, + + { + "comment": "verify payment exists via plan and is successful", + "request": { "uri": "me/plans/{{accountPlan.uuid}}/payments" }, + "response": { + "check": [ + {"condition": "json.length === 1"}, + {"condition": "json[0].getPlan() === '{{plans.[0].uuid}}'"}, + {"condition": "json[0].getAccountPlan() === '{{accountPlan.uuid}}'"}, + {"condition": "json[0].getPaymentObject().getAmount() === {{plans.[0].price}}"}, + {"condition": "json[0].getPaymentObject().getStatus().name() === 'success'"}, + {"condition": "json[0].getBillObject().getPlan() === '{{plans.[0].uuid}}'"}, + {"condition": "json[0].getBillObject().getAccountPlan() === '{{accountPlan.uuid}}'"}, + {"condition": "json[0].getBillObject().getQuantity() === 1"}, + {"condition": "json[0].getBillObject().getPrice() === {{plans.[0].price}}"}, + {"condition": "json[0].getBillObject().getTotal() === {{plans.[0].price}}"}, + {"condition": "json[0].getBillObject().getStatus().name() === 'paid'"} ] } } diff --git a/utils/cobbzilla-utils b/utils/cobbzilla-utils index f182aa1e..03f16e3d 160000 --- a/utils/cobbzilla-utils +++ b/utils/cobbzilla-utils @@ -1 +1 @@ -Subproject commit f182aa1e314b2105ebf789eba1b1b1f003ffd765 +Subproject commit 03f16e3d52cc380068a20da8136228fbf10cb7c7