diff --git a/bubble-server/src/main/java/bubble/ApiConstants.java b/bubble-server/src/main/java/bubble/ApiConstants.java index 5a96b6c1..02742261 100644 --- a/bubble-server/src/main/java/bubble/ApiConstants.java +++ b/bubble-server/src/main/java/bubble/ApiConstants.java @@ -172,6 +172,7 @@ public class ApiConstants { public static final String EP_RESTORE = "/restore"; public static final String EP_KEYS = "/keys"; public static final String EP_STATUS = "/status"; + public static final String EP_PROMOTIONS = PROMOTIONS_ENDPOINT; public static final String EP_FORK = "/fork"; public static final String DETECT_ENDPOINT = "/detect"; 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 513be13c..8c11a97d 100644 --- a/bubble-server/src/main/java/bubble/cloud/payment/PaymentDriverBase.java +++ b/bubble-server/src/main/java/bubble/cloud/payment/PaymentDriverBase.java @@ -8,13 +8,14 @@ import bubble.service.bill.PromotionService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import java.util.ArrayList; import java.util.List; -import java.util.Map; -import java.util.TreeMap; import static bubble.model.bill.AccountPayment.totalPayments; import static bubble.model.bill.PaymentMethodType.promotional_credit; import static org.cobbzilla.util.daemon.ZillaRuntime.empty; +import static org.cobbzilla.util.json.JsonUtil.COMPACT_MAPPER; +import static org.cobbzilla.util.json.JsonUtil.json; import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx; @Slf4j @@ -70,7 +71,10 @@ public abstract class PaymentDriverBase extends CloudServiceDriverBase imp return bill; } - @Override public boolean authorize(BubblePlan plan, String accountPlanUuid, AccountPaymentMethod paymentMethod) { + @Override public boolean authorize(BubblePlan plan, + String accountPlanUuid, + String billUuid, + AccountPaymentMethod paymentMethod) { return true; } @@ -95,9 +99,10 @@ public abstract class PaymentDriverBase extends CloudServiceDriverBase imp return true; } - final AccountPayment successfulPayment = accountPaymentDAO.findByAccountAndAccountPlanAndBillAndPaymentSuccess(accountPlan.getAccount(), accountPlanUuid, bill.getUuid()); - if (successfulPayment != null) { - log.warn("purchase: successful AccountPayment found (marking Bill "+bill.getUuid()+" as paid and returning true): " + successfulPayment.getUuid()); + final List successfulPayments = accountPaymentDAO.findByAccountAndAccountPlanAndBillAndPaid(accountPlan.getAccount(), accountPlanUuid, bill.getUuid()); + final int totalPayments = totalPayments(successfulPayments); + if (totalPayments >= bill.getTotal()) { + log.warn("purchase: sufficient successful AccountPayments found (marking Bill "+bill.getUuid()+" as paid and returning true): "+json(successfulPayments, COMPACT_MAPPER)); billDAO.update(bill.setStatus(BillStatus.paid)); return true; } @@ -105,37 +110,31 @@ public abstract class PaymentDriverBase extends CloudServiceDriverBase imp // If the current PaymentDriver is not for a promotional credit, // then check for AccountPaymentMethods associated with promotional credits // If we have one, use that payment driver instead. It may apply a partial payment. - long chargeAmount = bill.getTotal(); + long chargeAmount = bill.getTotal() - totalPayments; if (getPaymentMethodType() != promotional_credit) { - final List creditsApplied = accountPaymentDAO.findByAccountAndAccountPlanAndBillAndCreditAppliedSuccess(accountPlan.getAccount(), accountPlanUuid, billUuid); - // sanity check - if (totalPayments(creditsApplied) >= bill.getTotal()) { - log.warn("purchase: credit already applied sufficient to pay bill.total"); - } else { - final List accountPaymentMethods = paymentMethodDAO.findByAccountAndPromoAndNotDeleted(accountPlan.getAccount()); - final Map promos = new TreeMap<>(); - for (AccountPaymentMethod apm : accountPaymentMethods) { - if (!apm.hasPromotion()) { - log.warn("purchase: AccountPaymentMethod "+apm.getUuid()+" has type "+promotional_credit+" but promotion was null, skipping"); - continue; - } - final Promotion promo = promotionDAO.findByUuid(apm.getPromotion()); - if (promo == null) { - log.warn("purchase: AccountPaymentMethod "+apm.getUuid()+": promotion "+apm.getPromotion()+" does not exist"); - continue; - } - if (promo.inactive()) { - log.warn("purchase: AccountPaymentMethod "+apm.getUuid()+": promotion "+apm.getPromotion()+" is not active"); - continue; - } - promos.put(promo, apm); + final List accountPaymentMethods = paymentMethodDAO.findByAccountAndPromoAndNotDeleted(accountPlan.getAccount()); + final List promos = new ArrayList<>(); + for (AccountPaymentMethod apm : accountPaymentMethods) { + if (!apm.hasPromotion()) { + log.warn("purchase: AccountPaymentMethod "+apm.getUuid()+" has type "+promotional_credit+" but promotion was null, skipping"); + continue; + } + final Promotion promo = promotionDAO.findByUuid(apm.getPromotion()); + if (promo == null) { + log.warn("purchase: AccountPaymentMethod "+apm.getUuid()+": promotion "+apm.getPromotion()+" does not exist"); + continue; } - if (!promos.isEmpty()) { - chargeAmount = promoService.usePromotions(plan, accountPlan, bill, paymentMethod, this, promos, chargeAmount); - if (chargeAmount <= 0) { - log.info("purchase: chargeAmount="+chargeAmount+" after applying promotions, done"); - return true; - } + if (promo.inactive()) { + log.warn("purchase: AccountPaymentMethod "+apm.getUuid()+": promotion "+apm.getPromotion()+" is not active"); + continue; + } + promos.add(promo.setPaymentMethod(apm)); + } + if (!promos.isEmpty()) { + chargeAmount = promoService.usePromotions(plan, accountPlan, bill, paymentMethod, this, promos, chargeAmount); + if (chargeAmount <= 0) { + log.info("purchase: chargeAmount="+chargeAmount+" after applying promotions, done"); + return true; } } } @@ -167,24 +166,33 @@ public abstract class PaymentDriverBase extends CloudServiceDriverBase imp AccountPlan accountPlan, AccountPaymentMethod paymentMethod, ChargeResult chargeResult) { + final String accountUuid = accountPlan.getAccount(); + final String accountPlanUuid = accountPlan.getUuid(); + final String billUuid = bill.getUuid(); + // record the payment final AccountPayment accountPayment = accountPaymentDAO.create(new AccountPayment() .setType(paymentMethod.getPaymentMethodType() == promotional_credit ? AccountPaymentType.credit_applied : AccountPaymentType.payment) - .setAccount(accountPlan.getAccount()) + .setAccount(accountUuid) .setPlan(accountPlan.getPlan()) - .setAccountPlan(accountPlan.getUuid()) + .setAccountPlan(accountPlanUuid) .setPaymentMethod(paymentMethod.getUuid()) - .setBill(bill.getUuid()) + .setBill(billUuid) .setAmount(chargeResult.getAmountCharged()) .setCurrency(bill.getCurrency()) .setStatus(AccountPaymentStatus.success) .setInfo(chargeResult.getChargeId())); - // mark the bill as paid, enable the plan - billDAO.update(bill.setStatus(BillStatus.paid)); + final int total = totalPayments(accountPaymentDAO.findByAccountAndAccountPlanAndBillAndPaid(accountUuid, accountPlanUuid, billUuid)); + if (total >= bill.getTotal()) { + // mark the bill as paid, enable the plan + billDAO.update(bill.setStatus(BillStatus.paid)); + } else if (bill.getStatus() == BillStatus.unpaid) { + billDAO.update(bill.setStatus(BillStatus.partial_payment)); + } // if there are no unpaid bills, we can (re-)enable the plan - final List unpaidBills = billDAO.findUnpaidByAccountPlan(accountPlan.getUuid()); + final List unpaidBills = billDAO.findUnpaidByAccountPlan(accountPlanUuid); if (unpaidBills.isEmpty()) { accountPlanDAO.update(accountPlan.setEnabled(true)); } else { 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 58ea9eb9..65bd7fae 100644 --- a/bubble-server/src/main/java/bubble/cloud/payment/PaymentServiceDriver.java +++ b/bubble-server/src/main/java/bubble/cloud/payment/PaymentServiceDriver.java @@ -18,7 +18,7 @@ public interface PaymentServiceDriver extends CloudServiceDriver { default PaymentValidationResult claim(AccountPaymentMethod paymentMethod) { return notSupported("claim"); } default PaymentValidationResult claim(AccountPlan accountPlan) { return notSupported("claim"); } - boolean authorize(BubblePlan plan, String accountPlanUuid, AccountPaymentMethod paymentMethod); + boolean authorize(BubblePlan plan, String accountPlanUuid, String billUuid, AccountPaymentMethod paymentMethod); boolean cancelAuthorization(BubblePlan plan, String accountPlanUuid, AccountPaymentMethod paymentMethod); diff --git a/bubble-server/src/main/java/bubble/cloud/payment/delegate/DelegatedPaymentDriver.java b/bubble-server/src/main/java/bubble/cloud/payment/delegate/DelegatedPaymentDriver.java index 04adae08..6956d548 100644 --- a/bubble-server/src/main/java/bubble/cloud/payment/delegate/DelegatedPaymentDriver.java +++ b/bubble-server/src/main/java/bubble/cloud/payment/delegate/DelegatedPaymentDriver.java @@ -3,10 +3,7 @@ package bubble.cloud.payment.delegate; import bubble.cloud.DelegatedCloudServiceDriverBase; import bubble.cloud.payment.PaymentServiceDriver; import bubble.dao.cloud.CloudServiceDAO; -import bubble.model.bill.AccountPaymentMethod; -import bubble.model.bill.AccountPlan; -import bubble.model.bill.BubblePlan; -import bubble.model.bill.PaymentMethodType; +import bubble.model.bill.*; import bubble.model.cloud.BubbleNode; import bubble.model.cloud.CloudService; import bubble.notify.payment.*; @@ -56,13 +53,14 @@ public class DelegatedPaymentDriver extends DelegatedCloudServiceDriverBase impl new PaymentMethodClaimNotification(cloud.getName(), accountPlan)); } - @Override public boolean authorize(BubblePlan plan, String accountPlanUuid, AccountPaymentMethod paymentMethod) { + @Override public boolean authorize(BubblePlan plan, String accountPlanUuid, String billUuid, AccountPaymentMethod paymentMethod) { final BubbleNode delegate = getDelegateNode(); final PaymentResult result = notificationService.notifySync(delegate, payment_driver_authorize, new PaymentNotification() .setCloud(cloud.getName()) .setPlanUuid(plan.getUuid()) .setAccountPlanUuid(accountPlanUuid) + .setBillUuid(billUuid) .setPaymentMethodUuid(paymentMethod.getUuid())); return processResult(result); } diff --git a/bubble-server/src/main/java/bubble/cloud/payment/promo/PromotionalPaymentDriverBase.java b/bubble-server/src/main/java/bubble/cloud/payment/promo/PromotionalPaymentDriverBase.java index b704756d..4bd2cc39 100644 --- a/bubble-server/src/main/java/bubble/cloud/payment/promo/PromotionalPaymentDriverBase.java +++ b/bubble-server/src/main/java/bubble/cloud/payment/promo/PromotionalPaymentDriverBase.java @@ -28,6 +28,13 @@ public abstract class PromotionalPaymentDriverBase extends PaymentDriverBase< if (!paymentMethod.getCloud().equals(cloud.getUuid()) || paymentMethod.deleted()) { return new PaymentValidationResult("err.paymentMethodType.mismatch"); } + if (!paymentMethod.hasPromotion()) { + return new PaymentValidationResult("err.paymentMethodType.mismatch"); + } + final Promotion promotion = promotionDAO.findByUuid(paymentMethod.getPromotion()); + if (promotion == null || promotion.disabled()) { + return new PaymentValidationResult("err.paymentMethodType.mismatch"); + } return new PaymentValidationResult(paymentMethod); } @@ -53,15 +60,19 @@ public abstract class PromotionalPaymentDriverBase extends PaymentDriverBase< return ZERO_CHARGE; } + // mark deleted so it will not be found/applied for future transactions log.info("charge: applying promotion: "+paymentMethod.getPromotion()+" via AccountPaymentMethod: "+paymentMethod.getUuid()); paymentMethodDAO.update(paymentMethod.setDeleted()); + return getChargeResult(chargeAmount, promotion); + } + + protected ChargeResult getChargeResult(long chargeAmount, Promotion promotion) { // apply up to maximum if (chargeAmount > promotion.getMaxValue()) { log.warn("charge: chargeAmount ("+chargeAmount+") > promotion.maxValue ("+promotion.getMinValue()+"), returning maxValue"); return new ChargeResult().setAmountCharged(promotion.getMaxValue()).setChargeId(getClass().getSimpleName()); } else { - // mark deleted so it will not be found/applied for future transactions return new ChargeResult().setAmountCharged(chargeAmount).setChargeId(getClass().getSimpleName()); } } diff --git a/bubble-server/src/main/java/bubble/cloud/payment/promo/PromotionalPaymentServiceDriver.java b/bubble-server/src/main/java/bubble/cloud/payment/promo/PromotionalPaymentServiceDriver.java index 062cec4a..f66e8ef6 100644 --- a/bubble-server/src/main/java/bubble/cloud/payment/promo/PromotionalPaymentServiceDriver.java +++ b/bubble-server/src/main/java/bubble/cloud/payment/promo/PromotionalPaymentServiceDriver.java @@ -8,11 +8,13 @@ import bubble.model.bill.AccountPlan; import bubble.model.bill.Bill; import bubble.model.bill.Promotion; -import java.util.Map; +import java.util.List; import java.util.Set; public interface PromotionalPaymentServiceDriver extends PaymentServiceDriver { + default boolean adminAddPromoToAccount(Promotion promo, Account account) { return false; } + default boolean addPromoToAccount(Promotion promo, Account caller) { return false; } default boolean addReferralPromoToAccount(Promotion promo, @@ -20,15 +22,16 @@ public interface PromotionalPaymentServiceDriver extends PaymentServiceDriver { Account referredFrom, ReferralCode referralCode) { return false; } - default boolean canUseNow(Bill bill, Promotion promo, + default boolean canUseNow(Bill bill, + Promotion promo, PromotionalPaymentServiceDriver promoDriver, - Map promos, + List promos, Set usable, AccountPlan accountPlan, AccountPaymentMethod paymentMethod) { // do not use if deleted (should never happen) // do not use if other higher priority promotions are usable - return !paymentMethod.deleted() && usable.isEmpty(); + return paymentMethod.notDeleted() && usable.isEmpty(); } } diff --git a/bubble-server/src/main/java/bubble/cloud/payment/promo/accountCredit/AccountCreditPaymentConfig.java b/bubble-server/src/main/java/bubble/cloud/payment/promo/accountCredit/AccountCreditPaymentConfig.java new file mode 100644 index 00000000..24f3a7dd --- /dev/null +++ b/bubble-server/src/main/java/bubble/cloud/payment/promo/accountCredit/AccountCreditPaymentConfig.java @@ -0,0 +1,13 @@ +package bubble.cloud.payment.promo.accountCredit; + +import bubble.cloud.payment.promo.PromotionPaymentConfig; +import lombok.Getter; +import lombok.Setter; + +public class AccountCreditPaymentConfig extends PromotionPaymentConfig { + + @Getter @Setter private Integer creditAmount; + @Getter @Setter private Boolean fullBill; + public boolean fullBill () { return fullBill != null && fullBill; } + +} diff --git a/bubble-server/src/main/java/bubble/cloud/payment/promo/accountCredit/AccountCreditPaymentDriver.java b/bubble-server/src/main/java/bubble/cloud/payment/promo/accountCredit/AccountCreditPaymentDriver.java new file mode 100644 index 00000000..162facce --- /dev/null +++ b/bubble-server/src/main/java/bubble/cloud/payment/promo/accountCredit/AccountCreditPaymentDriver.java @@ -0,0 +1,43 @@ +package bubble.cloud.payment.promo.accountCredit; + +import bubble.cloud.payment.ChargeResult; +import bubble.cloud.payment.promo.PromotionalPaymentDriverBase; +import bubble.cloud.payment.promo.PromotionalPaymentServiceDriver; +import bubble.model.account.Account; +import bubble.model.bill.*; + +import java.util.List; +import java.util.Set; + +public class AccountCreditPaymentDriver extends PromotionalPaymentDriverBase { + + @Override public boolean adminAddPromoToAccount(Promotion promo, Account account) { + paymentMethodDAO.create(new AccountPaymentMethod() + .setAccount(account.getUuid()) + .setCloud(promo.getCloud()) + .setPaymentMethodType(PaymentMethodType.promotional_credit) + .setPaymentInfo(promo.getName()) + .setMaskedPaymentInfo(promo.getName()) + .setPromotion(promo.getUuid())); + return true; + } + + @Override public boolean canUseNow(Bill bill, + Promotion promo, + PromotionalPaymentServiceDriver promoDriver, + List promos, + Set usable, + AccountPlan accountPlan, + AccountPaymentMethod paymentMethod) { + return promo.getPaymentMethod().notDeleted(); + } + + @Override protected ChargeResult getChargeResult(long chargeAmount, Promotion promotion) { + if (config.fullBill()) { + return new ChargeResult().setAmountCharged(chargeAmount).setChargeId(getClass().getName()); + } + final int amount = chargeAmount > config.getCreditAmount() ? config.getCreditAmount() : (int) chargeAmount; + return super.getChargeResult(amount, promotion); + } + +} diff --git a/bubble-server/src/main/java/bubble/cloud/payment/promo/firstMonthFree/FirstMonthFreePaymentDriver.java b/bubble-server/src/main/java/bubble/cloud/payment/promo/firstMonthFree/FirstMonthFreePaymentDriver.java index 3d99f67b..f7419375 100644 --- a/bubble-server/src/main/java/bubble/cloud/payment/promo/firstMonthFree/FirstMonthFreePaymentDriver.java +++ b/bubble-server/src/main/java/bubble/cloud/payment/promo/firstMonthFree/FirstMonthFreePaymentDriver.java @@ -8,7 +8,6 @@ import bubble.model.bill.*; import lombok.extern.slf4j.Slf4j; import java.util.List; -import java.util.Map; import java.util.Set; import static org.cobbzilla.util.daemon.ZillaRuntime.empty; @@ -42,7 +41,7 @@ public class FirstMonthFreePaymentDriver extends PromotionalPaymentDriverBase promos, + List promos, Set usable, AccountPlan accountPlan, AccountPaymentMethod paymentMethod) { diff --git a/bubble-server/src/main/java/bubble/cloud/payment/promo/referralMonthFree/ReferralMonthFreePaymentDriver.java b/bubble-server/src/main/java/bubble/cloud/payment/promo/referralMonthFree/ReferralMonthFreePaymentDriver.java index 3997acd7..0af76fbb 100644 --- a/bubble-server/src/main/java/bubble/cloud/payment/promo/referralMonthFree/ReferralMonthFreePaymentDriver.java +++ b/bubble-server/src/main/java/bubble/cloud/payment/promo/referralMonthFree/ReferralMonthFreePaymentDriver.java @@ -12,7 +12,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import java.util.List; -import java.util.Map; import java.util.Set; import static org.cobbzilla.util.daemon.ZillaRuntime.empty; @@ -29,6 +28,11 @@ public class ReferralMonthFreePaymentDriver extends PromotionalPaymentDriverBase Account caller, Account referredFrom, ReferralCode referralCode) { + // sanity check + if (!promo.enabled()) { + log.warn("applyReferralPromo: promo="+promo.getName()+" is not enabled"); + return false; + } // caller must not have any bills final int billCount = billDAO.countByAccount(caller.getUuid()); @@ -92,13 +96,13 @@ public class ReferralMonthFreePaymentDriver extends PromotionalPaymentDriverBase @Override public boolean canUseNow(Bill bill, Promotion promo, PromotionalPaymentServiceDriver promoDriver, - Map promos, + List promos, Set usable, AccountPlan accountPlan, AccountPaymentMethod paymentMethod) { final String prefix = getClass().getSimpleName() + ".canUseNow: "; try { - final AccountPaymentMethod promoPaymentMethod = promos.get(promo); + final AccountPaymentMethod promoPaymentMethod = promo.getPaymentMethod(); final String referralCodeUuid = promoPaymentMethod.getPaymentInfo(); final ReferralCode referralCode = referralCodeDAO.findByUuid(referralCodeUuid); if (referralCode == null) { diff --git a/bubble-server/src/main/java/bubble/cloud/payment/stripe/StripePaymentDriver.java b/bubble-server/src/main/java/bubble/cloud/payment/stripe/StripePaymentDriver.java index e434db31..c360ea5f 100644 --- a/bubble-server/src/main/java/bubble/cloud/payment/stripe/StripePaymentDriver.java +++ b/bubble-server/src/main/java/bubble/cloud/payment/stripe/StripePaymentDriver.java @@ -11,7 +11,10 @@ import com.stripe.exception.CardException; import com.stripe.exception.StripeException; import com.stripe.model.*; import com.stripe.param.EventListParams; +import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.cobbzilla.wizard.cache.redis.RedisService; import org.cobbzilla.wizard.validation.SimpleViolationException; @@ -22,6 +25,7 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; +import static bubble.model.bill.AccountPayment.totalPayments; import static java.util.concurrent.TimeUnit.DAYS; import static java.util.concurrent.TimeUnit.HOURS; import static org.cobbzilla.util.daemon.ZillaRuntime.die; @@ -146,6 +150,7 @@ public class StripePaymentDriver extends PaymentDriverBase= price) { + log.warn("authorized: already paid, no need to authorize"); + return true; + } + + final long chargeAmount = price - paidSoFar; + chargeParams.put("amount", chargeAmount); // Amount in cents chargeParams.put("currency", plan.getCurrency().toLowerCase()); chargeParams.put("customer", paymentMethod.getPaymentInfo()); chargeParams.put("description", plan.chargeDescription()); @@ -165,8 +184,8 @@ public class StripePaymentDriver extends PaymentDriverBase { @@ -27,6 +29,16 @@ public class AccountPaymentDAO extends AccountOwnedEntityDAO { return findByFields("account", accountUuid, "accountPlan", accountPlanUuid); } + public List findByAccountAndAccountPlanAndPaid(String accountUuid, String accountPlanUuid) { + return list(criteria().add(and( + eq("account", accountUuid), + eq("accountPlan", accountPlanUuid), + eq("status", AccountPaymentStatus.success), + or(eq("type", AccountPaymentType.payment), + eq("type", AccountPaymentType.credit_applied))))); + + } + public List findByAccountAndAccountPlanAndBill(String accountUuid, String accountPlanUuid, String billUuid) { return findByFields("account", accountUuid, "accountPlan", accountPlanUuid, "bill", billUuid); } @@ -39,6 +51,16 @@ public class AccountPaymentDAO extends AccountOwnedEntityDAO { "status", AccountPaymentStatus.success); } + public List findByAccountAndAccountPlanAndBillAndPaid(String accountUuid, String accountPlanUuid, String billUuid) { + return list(criteria().add(and( + eq("account", accountUuid), + eq("accountPlan", accountPlanUuid), + eq("bill", billUuid), + eq("status", AccountPaymentStatus.success), + or(eq("type", AccountPaymentType.payment), + eq("type", AccountPaymentType.credit_applied))))); + } + public List findByAccountAndPaymentSuccess(String accountUuid) { return findByFields("account", accountUuid, "type", AccountPaymentType.payment, 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 f4560294..6b857f56 100644 --- a/bubble-server/src/main/java/bubble/dao/bill/AccountPlanDAO.java +++ b/bubble-server/src/main/java/bubble/dao/bill/AccountPlanDAO.java @@ -93,7 +93,7 @@ public class AccountPlanDAO extends AccountOwnedEntityDAO { if (paymentDriver.getPaymentMethodType().requiresAuth()) { final BubblePlan plan = planDAO.findByUuid(accountPlan.getPlan()); accountPlan.beforeCreate(); // ensure uuid exists - paymentDriver.authorize(plan, accountPlan.getUuid(), accountPlan.getPaymentMethodObject()); + paymentDriver.authorize(plan, accountPlan.getUuid(), null, accountPlan.getPaymentMethodObject()); } accountPlan.setPaymentMethod(accountPlan.getPaymentMethodObject().getUuid()); accountPlan.setNextBill(0L); // bill and payment occurs in postCreate, will update this 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 a168ee8c..f104293b 100644 --- a/bubble-server/src/main/java/bubble/dao/bill/BillDAO.java +++ b/bubble-server/src/main/java/bubble/dao/bill/BillDAO.java @@ -7,6 +7,8 @@ import org.springframework.stereotype.Repository; import java.util.List; +import static org.hibernate.criterion.Restrictions.*; + @Repository public class BillDAO extends AccountOwnedEntityDAO { @@ -26,11 +28,16 @@ public class BillDAO extends AccountOwnedEntityDAO { } public List findUnpaidByAccountPlan(String accountPlanUuid) { - return findByFields("accountPlan", accountPlanUuid, "status", BillStatus.unpaid); + return list(criteria().add(and( + eq("accountPlan", accountPlanUuid), + ne("status", BillStatus.paid))) + .addOrder(ORDER_CTIME_ASC)); } public List findUnpaidByAccount(String accountUuid) { - return findByFields("account", accountUuid, "status", BillStatus.unpaid); + return list(criteria().add(and( + eq("account", accountUuid), + ne("status", BillStatus.paid)))); } public Bill findOldestUnpaidBillByAccountPlan(String accountPlanUuid) { diff --git a/bubble-server/src/main/java/bubble/dao/bill/PromotionDAO.java b/bubble-server/src/main/java/bubble/dao/bill/PromotionDAO.java index 368c7040..114c85b9 100644 --- a/bubble-server/src/main/java/bubble/dao/bill/PromotionDAO.java +++ b/bubble-server/src/main/java/bubble/dao/bill/PromotionDAO.java @@ -33,12 +33,13 @@ public class PromotionDAO extends AbstractCRUDDAO { return filterActive(findByFields("enabled", true, "code", null, "referral", false)); } - public List findEnabledAndActiveWithNoCodeOrWithCode(String code) { + public List findVisibleAndEnabledAndActiveWithNoCodeOrWithCode(String code) { if (empty(code)) { - return filterActive(findByFields("enabled", true, "code", null)); + return filterActive(findByFields("enabled", true, "code", null, "visible", true)); } else { return filterActive(list(criteria().add(and( eq("enabled", true), + eq("visible", true), or(isNull("code"), eq("code", code)))))); } } diff --git a/bubble-server/src/main/java/bubble/model/bill/BillStatus.java b/bubble-server/src/main/java/bubble/model/bill/BillStatus.java index 0f4730b3..dc0cd172 100644 --- a/bubble-server/src/main/java/bubble/model/bill/BillStatus.java +++ b/bubble-server/src/main/java/bubble/model/bill/BillStatus.java @@ -6,7 +6,7 @@ import static bubble.ApiConstants.enumFromString; public enum BillStatus { - unpaid, paid; + unpaid, partial_payment, paid; @JsonCreator public static BillStatus fromString (String v) { return enumFromString(BillStatus.class, v); } diff --git a/bubble-server/src/main/java/bubble/model/bill/Promotion.java b/bubble-server/src/main/java/bubble/model/bill/Promotion.java index ec266417..04b90cd9 100644 --- a/bubble-server/src/main/java/bubble/model/bill/Promotion.java +++ b/bubble-server/src/main/java/bubble/model/bill/Promotion.java @@ -16,6 +16,9 @@ import org.cobbzilla.wizard.validation.HasValue; import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.Transient; + +import java.util.Comparator; import static bubble.ApiConstants.PROMOTIONS_ENDPOINT; import static org.cobbzilla.util.daemon.ZillaRuntime.now; @@ -31,8 +34,21 @@ public class Promotion extends IdentifiableBase public static final String[] CREATE_FIELDS = ArrayUtil.append(UPDATE_FIELDS, "name", "code", "referral", "currency", "maxValue"); + public static final Comparator SORT_PAYMENT_METHOD_CTIME = (Comparator) (p1, p2) -> { + if (p1.hasPaymentMethod() && p2.hasPaymentMethod()) { + return Long.compare(p1.getPaymentMethod().getCtime(), p2.getPaymentMethod().getCtime()); + } + return p1.compareTo(p2); + }; + public Promotion (Promotion other) { copy(this, other, CREATE_FIELDS); } + public Promotion(String id) { + // used for finding Promotion in AccountPromotionsResource and PromotionService + setUuid(id); + setName(id); + } + @Override public Identifiable update(Identifiable other) { copy(this, other, UPDATE_FIELDS); return this; } @Override public int compareTo(Promotion o) { @@ -62,33 +78,43 @@ public class Promotion extends IdentifiableBase @ECIndex @Column(nullable=false) @Getter @Setter private Boolean enabled = true; public boolean enabled () { return enabled == null || enabled; } + public boolean disabled () { return !enabled(); } @ECSearchable @ECField(index=60) + @ECIndex @Column(nullable=false) + @Getter @Setter private Boolean visible = false; + public boolean visible() { return visible == null || visible; } + public boolean invisible() { return !visible(); } + + @ECSearchable @ECField(index=70) @ECIndex @Getter @Setter private Long validFrom; public boolean hasStarted () { return validFrom == null || validFrom > now(); } - @ECSearchable @ECField(index=70) + @ECSearchable @ECField(index=80) @ECIndex @Getter @Setter private Long validTo; public boolean hasEnded () { return validTo != null && validTo > now(); } public boolean active () { return enabled() && hasStarted() && !hasEnded(); } public boolean inactive () { return !active(); } - @ECSearchable @ECField(index=80) + @ECSearchable @ECField(index=90) @ECIndex @Column(nullable=false) @Getter @Setter private Boolean referral = false; public boolean referral () { return referral != null && referral; } - @ECSearchable @ECField(index=90) + @ECSearchable @ECField(index=100) @ECIndex @Column(nullable=false, updatable=false, length=10) @Getter @Setter private String currency; - @ECSearchable @ECField(index=100) + @ECSearchable @ECField(index=110) @ECIndex @Column(nullable=false, updatable=false) - @Getter @Setter private Integer minValue = 5; + @Getter @Setter private Integer minValue = 100; - @ECSearchable @ECField(index=110) + @ECSearchable @ECField(index=120) @ECIndex @Column(nullable=false, updatable=false) @Getter @Setter private Integer maxValue; + @Transient @Getter @Setter private AccountPaymentMethod paymentMethod; + public boolean hasPaymentMethod () { return paymentMethod != null; } + } 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 index deb7a6f4..e2c23810 100644 --- 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 @@ -17,7 +17,7 @@ public class NotificationHandler_payment_driver_authorize extends NotificationHa final BubblePlan plan = planDAO.findByUuid(paymentNotification.getPlanUuid()); final AccountPaymentMethod paymentMethod = paymentMethodDAO.findByUuid(paymentNotification.getPaymentMethodUuid()); final PaymentServiceDriver paymentDriver = paymentService.getPaymentDriver(configuration); - return paymentDriver.authorize(plan, paymentNotification.getAccountPlanUuid(), paymentMethod); + return paymentDriver.authorize(plan, paymentNotification.getAccountPlanUuid(), paymentNotification.getBillUuid(), paymentMethod); } } diff --git a/bubble-server/src/main/java/bubble/resources/account/AccountPromotionsResource.java b/bubble-server/src/main/java/bubble/resources/account/AccountPromotionsResource.java new file mode 100644 index 00000000..734f4ea5 --- /dev/null +++ b/bubble-server/src/main/java/bubble/resources/account/AccountPromotionsResource.java @@ -0,0 +1,53 @@ +package bubble.resources.account; + +import bubble.model.account.Account; +import bubble.model.bill.Promotion; +import bubble.service.bill.PromotionService; +import org.glassfish.grizzly.http.server.Request; +import org.glassfish.jersey.server.ContainerRequest; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.ws.rs.*; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; + +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.cobbzilla.wizard.resources.ResourceUtil.*; + +@Consumes(APPLICATION_JSON) +@Produces(APPLICATION_JSON) +public class AccountPromotionsResource { + + @Autowired private PromotionService promoService; + + private Account account; + + public AccountPromotionsResource (Account account) { this.account = account; } + + @GET + public Response listPromotions(@Context Request req, + @Context ContainerRequest ctx) { + final Account caller = userPrincipal(ctx); + if (!caller.admin() && !caller.getUuid().equals(account.getUuid())) return forbidden(); + return ok(promoService.listPromosForAccount(account.getUuid())); + } + + @PUT + public Response adminAddPromotion(@Context Request req, + @Context ContainerRequest ctx, + Promotion request) { + final Account caller = userPrincipal(ctx); + if (!caller.admin()) return forbidden(); + return ok(promoService.adminAddPromotion(account, request)); + } + + @DELETE @Path("/{id}") + public Response adminRemovePromotion(@Context Request req, + @Context ContainerRequest ctx, + @PathParam("id") String id) { + final Account caller = userPrincipal(ctx); + if (!caller.admin()) return forbidden(); + return ok(promoService.adminRemovePromotion(account, new Promotion(id))); + } + +} diff --git a/bubble-server/src/main/java/bubble/resources/account/AccountsResource.java b/bubble-server/src/main/java/bubble/resources/account/AccountsResource.java index a9de4b6a..e12a3b4e 100644 --- a/bubble-server/src/main/java/bubble/resources/account/AccountsResource.java +++ b/bubble-server/src/main/java/bubble/resources/account/AccountsResource.java @@ -169,10 +169,18 @@ public class AccountsResource { return ok(networkService.listLaunchStatuses(c.account.getUuid())); } + @Path("/{id}"+EP_PROMOTIONS) + public AccountPromotionsResource getPromotionsResource(@Context Request req, + @Context ContainerRequest ctx, + @PathParam("id") String id) { + final AccountContext c = new AccountContext(ctx, id); + return configuration.subResource(AccountPromotionsResource.class, c.account); + } + @GET @Path("/{id}"+EP_POLICY) public Response viewPolicy(@Context ContainerRequest ctx, @PathParam("id") String id) { - final AccountContext c = new AccountContext(ctx, id); + final AccountsResource.AccountContext c = new AccountsResource.AccountContext(ctx, id); final AccountPolicy policy = policyDAO.findSingleByAccount(c.account.getUuid()); authenticatorService.ensureAuthenticated(ctx, policy, ActionTarget.account); return policy == null ? notFound(id) : ok(policy.mask()); diff --git a/bubble-server/src/main/java/bubble/resources/account/MeResource.java b/bubble-server/src/main/java/bubble/resources/account/MeResource.java index a19a5339..2203bc75 100644 --- a/bubble-server/src/main/java/bubble/resources/account/MeResource.java +++ b/bubble-server/src/main/java/bubble/resources/account/MeResource.java @@ -351,6 +351,13 @@ public class MeResource { return ok(networkService.listLaunchStatuses(caller.getUuid())); } + @Path(EP_PROMOTIONS) + public AccountPromotionsResource getPromotionsResource(@Context Request req, + @Context ContainerRequest ctx) { + final Account caller = userPrincipal(ctx); + return configuration.subResource(AccountPromotionsResource.class, caller); + } + @Autowired private BubbleModelSetupService modelSetupService; @POST @Path(EP_MODEL) 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 3cb51919..6992ceee 100644 --- a/bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java +++ b/bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java @@ -13,9 +13,7 @@ import bubble.dao.cloud.BubbleNetworkDAO; import bubble.dao.cloud.CloudServiceDAO; import bubble.model.account.Account; import bubble.model.account.AccountSshKey; -import bubble.model.bill.AccountPaymentMethod; -import bubble.model.bill.AccountPlan; -import bubble.model.bill.BubblePlan; +import bubble.model.bill.*; import bubble.model.cloud.BubbleDomain; import bubble.model.cloud.BubbleFootprint; import bubble.model.cloud.BubbleNetwork; @@ -187,8 +185,14 @@ public class AccountPlansResource extends AccountOwnedResource listPromosForAccount(String accountUuid) { + final List promos = new ArrayList<>(); + final List apmList = accountPaymentMethodDAO.findByAccountAndPromoAndNotDeleted(accountUuid); + for (AccountPaymentMethod apm : apmList) { + final Promotion promo = promotionDAO.findByUuid(apm.getPromotion()); + if (promo == null) { + log.warn("listPromos: promo "+apm.getPromotion()+" not found for apm="+apm.getUuid()); + continue; + } + if (!promo.enabled() && !promo.getVisible()) { + log.warn("listPromos: promo "+apm.getPromotion()+" is not enabled and not admin-assigned, apm="+apm.getUuid()); + continue; + } + promos.add(promo.setPaymentMethod(apm)); + } + promos.sort(SORT_PAYMENT_METHOD_CTIME); + return promos; + } + + public List adminAddPromotion(Account account, Promotion request) { + final Promotion promotion = request.hasUuid() + ? promotionDAO.findByUuid(request.getUuid()) + : promotionDAO.findByName(request.getName()); + if (promotion == null) throw notFoundEx(json(request)); + final var promoDriver = (PromotionalPaymentServiceDriver) cloudDAO.findByUuid(promotion.getCloud()).getPaymentDriver(configuration); + promoDriver.adminAddPromoToAccount(promotion, account); + return listPromosForAccount(account.getUuid()); + } + + public List adminRemovePromotion(Account account, Promotion request) { + + final Promotion promotion = request.hasUuid() + ? promotionDAO.findByUuid(request.getUuid()) + : promotionDAO.findByName(request.getName()); + if (promotion == null) throw notFoundEx(json(request)); + + final AccountPaymentMethod paymentMethod = accountPaymentMethodDAO.findByAccount(account.getUuid()).stream() + .filter(apm -> apm.hasPromotion() && apm.getPromotion().equals(promotion.getUuid())) + .findFirst().orElse(null); + if (paymentMethod == null) throw notFoundEx(promotion.getName()); + + accountPaymentMethodDAO.update(paymentMethod.setDeleted()); + + return listPromosForAccount(account.getUuid()); + } } diff --git a/bubble-server/src/main/resources/message_templates/en_US/server/post_auth/ResourceMessages.properties b/bubble-server/src/main/resources/message_templates/en_US/server/post_auth/ResourceMessages.properties index e0213588..40bb051d 100644 --- a/bubble-server/src/main/resources/message_templates/en_US/server/post_auth/ResourceMessages.properties +++ b/bubble-server/src/main/resources/message_templates/en_US/server/post_auth/ResourceMessages.properties @@ -600,6 +600,7 @@ err.purchase.cardProcessingError=Error processing credit card payment err.purchase.cardUnknownError=Error processing credit card payment err.purchase.chargePendingError=Error processing credit card payment, charge is pending err.purchase.chargeFailedError=Error processing credit card payment, charge failed +err.purchase.chargeAmountMismatch=Charge amounts were mismatched, cannot purchase err.purchase.currencyMismatch=Payment currency does not match bill currency err.purchase.paymentMethodMismatch=Payment method cannot be used for this purchase err.purchase.paymentMethodNotFound=Payment method not found diff --git a/bubble-server/src/test/java/bubble/mock/MockStripePaymentDriver.java b/bubble-server/src/test/java/bubble/mock/MockStripePaymentDriver.java index 28e5e5ec..d1667a59 100644 --- a/bubble-server/src/test/java/bubble/mock/MockStripePaymentDriver.java +++ b/bubble-server/src/test/java/bubble/mock/MockStripePaymentDriver.java @@ -21,12 +21,12 @@ public class MockStripePaymentDriver extends StripePaymentDriver { Stripe.apiKey = getCredentials().getParam(PARAM_SECRET_API_KEY);; } - @Override public boolean authorize(BubblePlan plan, String accountPlanUuid, AccountPaymentMethod paymentMethod) { + @Override public boolean authorize(BubblePlan plan, String accountPlanUuid, String billUuid, AccountPaymentMethod paymentMethod) { final String err = error.get(); if (err != null && (err.equals("authorize") || err.equals("all"))) { throw invalidEx("err.purchase.authNotFound", "mock: error flag="+err); } else { - return super.authorize(plan, accountPlanUuid, paymentMethod); + return super.authorize(plan, accountPlanUuid, billUuid, paymentMethod); } } diff --git a/bubble-server/src/test/java/bubble/test/ProxyTest.java b/bubble-server/src/test/java/bubble/test/filter/ProxyTest.java similarity index 96% rename from bubble-server/src/test/java/bubble/test/ProxyTest.java rename to bubble-server/src/test/java/bubble/test/filter/ProxyTest.java index dcb480a2..40f89baa 100644 --- a/bubble-server/src/test/java/bubble/test/ProxyTest.java +++ b/bubble-server/src/test/java/bubble/test/filter/ProxyTest.java @@ -1,6 +1,7 @@ -package bubble.test; +package bubble.test.filter; import bubble.server.BubbleConfiguration; +import bubble.test.ActivatedBubbleModelTestBase; import org.cobbzilla.wizard.server.RestServer; import org.cobbzilla.wizard.util.RestResponse; import org.junit.Test; diff --git a/bubble-server/src/test/java/bubble/test/TrafficAnalyticsTest.java b/bubble-server/src/test/java/bubble/test/filter/TrafficAnalyticsTest.java similarity index 79% rename from bubble-server/src/test/java/bubble/test/TrafficAnalyticsTest.java rename to bubble-server/src/test/java/bubble/test/filter/TrafficAnalyticsTest.java index 2c2fbf8a..e72ccfb8 100644 --- a/bubble-server/src/test/java/bubble/test/TrafficAnalyticsTest.java +++ b/bubble-server/src/test/java/bubble/test/filter/TrafficAnalyticsTest.java @@ -1,5 +1,6 @@ -package bubble.test; +package bubble.test.filter; +import bubble.test.system.NetworkTestBase; import org.junit.Test; public class TrafficAnalyticsTest extends NetworkTestBase { diff --git a/bubble-server/src/test/java/bubble/test/live/GoDaddyDnsTest.java b/bubble-server/src/test/java/bubble/test/live/GoDaddyDnsTest.java index 9e282672..32c6fa99 100644 --- a/bubble-server/src/test/java/bubble/test/live/GoDaddyDnsTest.java +++ b/bubble-server/src/test/java/bubble/test/live/GoDaddyDnsTest.java @@ -1,6 +1,6 @@ package bubble.test.live; -import bubble.test.NetworkTestBase; +import bubble.test.system.NetworkTestBase; import org.junit.Test; public class GoDaddyDnsTest extends NetworkTestBase { diff --git a/bubble-server/src/test/java/bubble/test/live/LiveTestBase.java b/bubble-server/src/test/java/bubble/test/live/LiveTestBase.java index 5396c12f..5b18f485 100644 --- a/bubble-server/src/test/java/bubble/test/live/LiveTestBase.java +++ b/bubble-server/src/test/java/bubble/test/live/LiveTestBase.java @@ -1,7 +1,7 @@ package bubble.test.live; import bubble.notify.NewNodeNotification; -import bubble.test.NetworkTestBase; +import bubble.test.system.NetworkTestBase; import lombok.extern.slf4j.Slf4j; import org.junit.After; import org.junit.AfterClass; diff --git a/bubble-server/src/test/java/bubble/test/live/Route53DnsTest.java b/bubble-server/src/test/java/bubble/test/live/Route53DnsTest.java index 55017f03..5d6be5ab 100644 --- a/bubble-server/src/test/java/bubble/test/live/Route53DnsTest.java +++ b/bubble-server/src/test/java/bubble/test/live/Route53DnsTest.java @@ -4,7 +4,7 @@ import bubble.cloud.CloudServiceDriver; import bubble.cloud.dns.route53.Route53DnsDriver; import bubble.model.cloud.BubbleDomain; import bubble.model.cloud.CloudService; -import bubble.test.NetworkTestBase; +import bubble.test.system.NetworkTestBase; import org.junit.Test; import java.util.Arrays; diff --git a/bubble-server/src/test/java/bubble/test/live/S3StorageTest.java b/bubble-server/src/test/java/bubble/test/live/S3StorageTest.java index 9b60352f..9e707ba1 100644 --- a/bubble-server/src/test/java/bubble/test/live/S3StorageTest.java +++ b/bubble-server/src/test/java/bubble/test/live/S3StorageTest.java @@ -7,7 +7,7 @@ import bubble.model.cloud.CloudService; import bubble.model.cloud.RekeyRequest; import bubble.model.cloud.StorageMetadata; import bubble.notify.storage.StorageListing; -import bubble.test.NetworkTestBase; +import bubble.test.system.NetworkTestBase; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.RandomUtils; import org.apache.http.HttpHeaders; diff --git a/bubble-server/src/test/java/bubble/test/PaymentTest.java b/bubble-server/src/test/java/bubble/test/payment/PaymentTest.java similarity index 95% rename from bubble-server/src/test/java/bubble/test/PaymentTest.java rename to bubble-server/src/test/java/bubble/test/payment/PaymentTest.java index 0da00ba5..4919fa31 100644 --- a/bubble-server/src/test/java/bubble/test/PaymentTest.java +++ b/bubble-server/src/test/java/bubble/test/payment/PaymentTest.java @@ -1,4 +1,4 @@ -package bubble.test; +package bubble.test.payment; import lombok.extern.slf4j.Slf4j; import org.junit.Test; diff --git a/bubble-server/src/test/java/bubble/test/PaymentTestBase.java b/bubble-server/src/test/java/bubble/test/payment/PaymentTestBase.java similarity index 93% rename from bubble-server/src/test/java/bubble/test/PaymentTestBase.java rename to bubble-server/src/test/java/bubble/test/payment/PaymentTestBase.java index c2e61340..53faf13e 100644 --- a/bubble-server/src/test/java/bubble/test/PaymentTestBase.java +++ b/bubble-server/src/test/java/bubble/test/payment/PaymentTestBase.java @@ -1,8 +1,9 @@ -package bubble.test; +package bubble.test.payment; import bubble.server.BubbleConfiguration; import bubble.service.bill.BillingService; import bubble.service.bill.StandardRefundService; +import bubble.test.ActivatedBubbleModelTestBase; import org.cobbzilla.wizard.server.RestServer; public class PaymentTestBase extends ActivatedBubbleModelTestBase { diff --git a/bubble-server/src/test/java/bubble/test/RecurringBillingTest.java b/bubble-server/src/test/java/bubble/test/payment/RecurringBillingTest.java similarity index 87% rename from bubble-server/src/test/java/bubble/test/RecurringBillingTest.java rename to bubble-server/src/test/java/bubble/test/payment/RecurringBillingTest.java index 26d174f9..c6a2f9b6 100644 --- a/bubble-server/src/test/java/bubble/test/RecurringBillingTest.java +++ b/bubble-server/src/test/java/bubble/test/payment/RecurringBillingTest.java @@ -1,4 +1,4 @@ -package bubble.test; +package bubble.test.payment; import org.junit.Test; diff --git a/bubble-server/src/test/java/bubble/test/promo/AccountCreditTest.java b/bubble-server/src/test/java/bubble/test/promo/AccountCreditTest.java new file mode 100644 index 00000000..cbbf029d --- /dev/null +++ b/bubble-server/src/test/java/bubble/test/promo/AccountCreditTest.java @@ -0,0 +1,12 @@ +package bubble.test.promo; + +import bubble.test.payment.PaymentTestBase; +import org.junit.Test; + +public class AccountCreditTest extends PaymentTestBase { + + @Override protected String getManifest() { return "promo/credit/manifest_credit"; } + + @Test public void testAccountCredit () throws Exception { modelTest("promo/account_credit"); } + +} diff --git a/bubble-server/src/test/java/bubble/test/FirstMonthAndReferralMonthPromotionTest.java b/bubble-server/src/test/java/bubble/test/promo/FirstMonthAndReferralMonthPromotionTest.java similarity index 81% rename from bubble-server/src/test/java/bubble/test/FirstMonthAndReferralMonthPromotionTest.java rename to bubble-server/src/test/java/bubble/test/promo/FirstMonthAndReferralMonthPromotionTest.java index fedf9da7..2709513f 100644 --- a/bubble-server/src/test/java/bubble/test/FirstMonthAndReferralMonthPromotionTest.java +++ b/bubble-server/src/test/java/bubble/test/promo/FirstMonthAndReferralMonthPromotionTest.java @@ -1,5 +1,6 @@ -package bubble.test; +package bubble.test.promo; +import bubble.test.payment.PaymentTestBase; import lombok.extern.slf4j.Slf4j; import org.junit.Test; diff --git a/bubble-server/src/test/java/bubble/test/FirstMonthFreePromotionTest.java b/bubble-server/src/test/java/bubble/test/promo/FirstMonthFreePromotionTest.java similarity index 61% rename from bubble-server/src/test/java/bubble/test/FirstMonthFreePromotionTest.java rename to bubble-server/src/test/java/bubble/test/promo/FirstMonthFreePromotionTest.java index 8c61ba6d..7ccd3c91 100644 --- a/bubble-server/src/test/java/bubble/test/FirstMonthFreePromotionTest.java +++ b/bubble-server/src/test/java/bubble/test/promo/FirstMonthFreePromotionTest.java @@ -1,12 +1,13 @@ -package bubble.test; +package bubble.test.promo; +import bubble.test.payment.PaymentTestBase; import lombok.extern.slf4j.Slf4j; import org.junit.Test; @Slf4j public class FirstMonthFreePromotionTest extends PaymentTestBase { - @Override protected String getManifest() { return "manifest-1mo-promo"; } + @Override protected String getManifest() { return "promo/1mo/manifest_1mo"; } @Test public void testFirstMonthFree () throws Exception { modelTest("promo/first_month_free"); } diff --git a/bubble-server/src/test/java/bubble/test/ReferralMonthFreePromotionTest.java b/bubble-server/src/test/java/bubble/test/promo/ReferralMonthFreePromotionTest.java similarity index 60% rename from bubble-server/src/test/java/bubble/test/ReferralMonthFreePromotionTest.java rename to bubble-server/src/test/java/bubble/test/promo/ReferralMonthFreePromotionTest.java index a25e165f..511e4b26 100644 --- a/bubble-server/src/test/java/bubble/test/ReferralMonthFreePromotionTest.java +++ b/bubble-server/src/test/java/bubble/test/promo/ReferralMonthFreePromotionTest.java @@ -1,12 +1,13 @@ -package bubble.test; +package bubble.test.promo; +import bubble.test.payment.PaymentTestBase; import lombok.extern.slf4j.Slf4j; import org.junit.Test; @Slf4j public class ReferralMonthFreePromotionTest extends PaymentTestBase { - @Override protected String getManifest() { return "manifest-referral-promo"; } + @Override protected String getManifest() { return "promo/referral/manifest_referral"; } @Test public void testReferralMonthFree () throws Exception { modelTest("promo/referral_month_free"); } diff --git a/bubble-server/src/test/java/bubble/test/AuthTest.java b/bubble-server/src/test/java/bubble/test/system/AuthTest.java similarity index 95% rename from bubble-server/src/test/java/bubble/test/AuthTest.java rename to bubble-server/src/test/java/bubble/test/system/AuthTest.java index a861108a..4c013366 100644 --- a/bubble-server/src/test/java/bubble/test/AuthTest.java +++ b/bubble-server/src/test/java/bubble/test/system/AuthTest.java @@ -1,7 +1,8 @@ -package bubble.test; +package bubble.test.system; import bubble.dao.account.AccountDAO; import bubble.model.account.Account; +import bubble.test.ActivatedBubbleModelTestBase; import lombok.extern.slf4j.Slf4j; import org.cobbzilla.wizard.model.HashedPassword; import org.junit.Before; diff --git a/bubble-server/src/test/java/bubble/test/BackupTest.java b/bubble-server/src/test/java/bubble/test/system/BackupTest.java similarity index 89% rename from bubble-server/src/test/java/bubble/test/BackupTest.java rename to bubble-server/src/test/java/bubble/test/system/BackupTest.java index 95a9943b..3eca6b3a 100644 --- a/bubble-server/src/test/java/bubble/test/BackupTest.java +++ b/bubble-server/src/test/java/bubble/test/system/BackupTest.java @@ -1,4 +1,4 @@ -package bubble.test; +package bubble.test.system; import org.junit.Test; diff --git a/bubble-server/src/test/java/bubble/test/DriverTest.java b/bubble-server/src/test/java/bubble/test/system/DriverTest.java similarity index 76% rename from bubble-server/src/test/java/bubble/test/DriverTest.java rename to bubble-server/src/test/java/bubble/test/system/DriverTest.java index 3e3ec74d..b6250fa8 100644 --- a/bubble-server/src/test/java/bubble/test/DriverTest.java +++ b/bubble-server/src/test/java/bubble/test/system/DriverTest.java @@ -1,5 +1,6 @@ -package bubble.test; +package bubble.test.system; +import bubble.test.ActivatedBubbleModelTestBase; import org.junit.Test; public class DriverTest extends ActivatedBubbleModelTestBase { diff --git a/bubble-server/src/test/java/bubble/test/LiveNetworkTest.java b/bubble-server/src/test/java/bubble/test/system/LiveNetworkTest.java similarity index 90% rename from bubble-server/src/test/java/bubble/test/LiveNetworkTest.java rename to bubble-server/src/test/java/bubble/test/system/LiveNetworkTest.java index fc104f97..56e746db 100644 --- a/bubble-server/src/test/java/bubble/test/LiveNetworkTest.java +++ b/bubble-server/src/test/java/bubble/test/system/LiveNetworkTest.java @@ -1,9 +1,8 @@ -package bubble.test; +package bubble.test.system; import bubble.cloud.CloudServiceDriver; import bubble.cloud.storage.s3.S3StorageDriver; import lombok.extern.slf4j.Slf4j; -import org.junit.Test; @Slf4j public class LiveNetworkTest extends NetworkTestBase { diff --git a/bubble-server/src/test/java/bubble/test/NetworkTest.java b/bubble-server/src/test/java/bubble/test/system/NetworkTest.java similarity index 93% rename from bubble-server/src/test/java/bubble/test/NetworkTest.java rename to bubble-server/src/test/java/bubble/test/system/NetworkTest.java index 9cd3091b..b75f38b1 100644 --- a/bubble-server/src/test/java/bubble/test/NetworkTest.java +++ b/bubble-server/src/test/java/bubble/test/system/NetworkTest.java @@ -1,4 +1,4 @@ -package bubble.test; +package bubble.test.system; import lombok.extern.slf4j.Slf4j; import org.junit.Test; diff --git a/bubble-server/src/test/java/bubble/test/NetworkTestBase.java b/bubble-server/src/test/java/bubble/test/system/NetworkTestBase.java similarity index 65% rename from bubble-server/src/test/java/bubble/test/NetworkTestBase.java rename to bubble-server/src/test/java/bubble/test/system/NetworkTestBase.java index 6bd988c2..9d2fdc88 100644 --- a/bubble-server/src/test/java/bubble/test/NetworkTestBase.java +++ b/bubble-server/src/test/java/bubble/test/system/NetworkTestBase.java @@ -1,4 +1,6 @@ -package bubble.test; +package bubble.test.system; + +import bubble.test.ActivatedBubbleModelTestBase; public class NetworkTestBase extends ActivatedBubbleModelTestBase { diff --git a/bubble-server/src/test/resources/models/manifest-1mo-promo.json b/bubble-server/src/test/resources/models/manifest-1mo-promo.json deleted file mode 100644 index 967ac880..00000000 --- a/bubble-server/src/test/resources/models/manifest-1mo-promo.json +++ /dev/null @@ -1,5 +0,0 @@ -[ - "manifest-test", - "system/cloudService_1mo_free", - "system/promotion_1mo_free" -] \ No newline at end of file diff --git a/bubble-server/src/test/resources/models/manifest-referral-promo.json b/bubble-server/src/test/resources/models/manifest-referral-promo.json deleted file mode 100644 index d17c6073..00000000 --- a/bubble-server/src/test/resources/models/manifest-referral-promo.json +++ /dev/null @@ -1,5 +0,0 @@ -[ - "manifest-test", - "system/cloudService_referral_free", - "system/promotion_referral_free" -] \ No newline at end of file diff --git a/bubble-server/src/test/resources/models/system/cloudService_1mo_free.json b/bubble-server/src/test/resources/models/promo/1mo/cloudService_1mo.json similarity index 89% rename from bubble-server/src/test/resources/models/system/cloudService_1mo_free.json rename to bubble-server/src/test/resources/models/promo/1mo/cloudService_1mo.json index 57b53be3..249f75ce 100644 --- a/bubble-server/src/test/resources/models/system/cloudService_1mo_free.json +++ b/bubble-server/src/test/resources/models/promo/1mo/cloudService_1mo.json @@ -5,6 +5,6 @@ "driverClass": "bubble.cloud.payment.promo.firstMonthFree.FirstMonthFreePaymentDriver", "driverConfig": {}, "credentials": {}, - "template": true + "template": false } ] \ No newline at end of file diff --git a/bubble-server/src/test/resources/models/promo/1mo/manifest_1mo.json b/bubble-server/src/test/resources/models/promo/1mo/manifest_1mo.json new file mode 100644 index 00000000..3cb5b3bc --- /dev/null +++ b/bubble-server/src/test/resources/models/promo/1mo/manifest_1mo.json @@ -0,0 +1,5 @@ +[ + "manifest-test", + "promo/1mo/cloudService_1mo", + "promo/1mo/promotion_1mo" +] \ No newline at end of file diff --git a/bubble-server/src/test/resources/models/system/promotion_1mo_free.json b/bubble-server/src/test/resources/models/promo/1mo/promotion_1mo.json similarity index 100% rename from bubble-server/src/test/resources/models/system/promotion_1mo_free.json rename to bubble-server/src/test/resources/models/promo/1mo/promotion_1mo.json diff --git a/bubble-server/src/test/resources/models/promo/credit/cloudService_credit.json b/bubble-server/src/test/resources/models/promo/credit/cloudService_credit.json new file mode 100644 index 00000000..15b26d34 --- /dev/null +++ b/bubble-server/src/test/resources/models/promo/credit/cloudService_credit.json @@ -0,0 +1,32 @@ +[ + { + "name": "AccountCredit1", + "type": "payment", + "driverClass": "bubble.cloud.payment.promo.accountCredit.AccountCreditPaymentDriver", + "driverConfig": { + "creditAmount": 100 + }, + "credentials": {}, + "template": false + }, + { + "name": "AccountCredit5", + "type": "payment", + "driverClass": "bubble.cloud.payment.promo.accountCredit.AccountCreditPaymentDriver", + "driverConfig": { + "creditAmount": 500 + }, + "credentials": {}, + "template": false + }, + { + "name": "AccountCreditBill", + "type": "payment", + "driverClass": "bubble.cloud.payment.promo.accountCredit.AccountCreditPaymentDriver", + "driverConfig": { + "fullBill": true + }, + "credentials": {}, + "template": false + } +] \ No newline at end of file diff --git a/bubble-server/src/test/resources/models/promo/credit/manifest_credit.json b/bubble-server/src/test/resources/models/promo/credit/manifest_credit.json new file mode 100644 index 00000000..624e4f63 --- /dev/null +++ b/bubble-server/src/test/resources/models/promo/credit/manifest_credit.json @@ -0,0 +1,5 @@ +[ + "manifest-test", + "promo/credit/cloudService_credit", + "promo/credit/promotion_credit" +] \ No newline at end of file diff --git a/bubble-server/src/test/resources/models/promo/credit/promotion_credit.json b/bubble-server/src/test/resources/models/promo/credit/promotion_credit.json new file mode 100644 index 00000000..d66432c2 --- /dev/null +++ b/bubble-server/src/test/resources/models/promo/credit/promotion_credit.json @@ -0,0 +1,26 @@ +[ + { + "name": "AccountCredit1", + "cloud": "AccountCredit1", + "priority": 1, + "currency": "USD", + "maxValue": 100, + "visible": false + }, + { + "name": "AccountCredit5", + "cloud": "AccountCredit5", + "priority": 1, + "currency": "USD", + "maxValue": 500, + "visible": false + }, + { + "name": "AccountCreditBill", + "cloud": "AccountCreditBill", + "priority": 1, + "currency": "USD", + "maxValue": 10000, + "visible": false + } +] \ No newline at end of file diff --git a/bubble-server/src/test/resources/models/system/cloudService_referral_free.json b/bubble-server/src/test/resources/models/promo/referral/cloudService_referral.json similarity index 90% rename from bubble-server/src/test/resources/models/system/cloudService_referral_free.json rename to bubble-server/src/test/resources/models/promo/referral/cloudService_referral.json index 67e51295..ce797a27 100644 --- a/bubble-server/src/test/resources/models/system/cloudService_referral_free.json +++ b/bubble-server/src/test/resources/models/promo/referral/cloudService_referral.json @@ -5,6 +5,6 @@ "driverClass": "bubble.cloud.payment.promo.referralMonthFree.ReferralMonthFreePaymentDriver", "driverConfig": {}, "credentials": {}, - "template": true + "template": false } ] \ No newline at end of file diff --git a/bubble-server/src/test/resources/models/promo/referral/manifest_referral.json b/bubble-server/src/test/resources/models/promo/referral/manifest_referral.json new file mode 100644 index 00000000..3c9195df --- /dev/null +++ b/bubble-server/src/test/resources/models/promo/referral/manifest_referral.json @@ -0,0 +1,5 @@ +[ + "manifest-test", + "promo/referral/cloudService_referral", + "promo/referral/promotion_referral" +] \ No newline at end of file diff --git a/bubble-server/src/test/resources/models/system/promotion_referral_free.json b/bubble-server/src/test/resources/models/promo/referral/promotion_referral.json similarity index 100% rename from bubble-server/src/test/resources/models/system/promotion_referral_free.json rename to bubble-server/src/test/resources/models/promo/referral/promotion_referral.json diff --git a/bubble-server/src/test/resources/models/tests/promo/account_credit.json b/bubble-server/src/test/resources/models/tests/promo/account_credit.json new file mode 100644 index 00000000..69a91c00 --- /dev/null +++ b/bubble-server/src/test/resources/models/tests/promo/account_credit.json @@ -0,0 +1,554 @@ +[ + { + "comment": "user: register a user account", + "request": { + "session": "new", + "uri": "auth/register", + "entity": { + "name": "test_user_small_promo", + "password": "password1!", + "contact": {"type": "email", "info": "test_user_small_promo@example.com"} + } + }, + "response": { + "store": "testAccount", + "sessionName": "userSession", + "session": "token" + } + }, + + { + "before": "sleep 10s", + "comment": "root: check email inbox for verification message", + "request": { + "session": "rootSession", + "uri": "debug/inbox/email/test_user_small_promo@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": "user: approve email verification request", + "request": { + "session": "userSession", + "uri": "auth/approve/{{emailInbox.[0].ctx.confirmationToken}}", + "method": "post" + } + }, + + { + "comment": "user: get plans", + "request": { "uri": "plans" }, + "response": { + "store": "plans", + "check": [{"condition": "json.length >= 1"}] + } + }, + + { + "comment": "user: list all promos available, should be none", + "request": {"uri": "promos"}, + "response": { + "check": [ {"condition": "json.length === 0"} ] + } + }, + + { + "comment": "user: list promos for account, should be none", + "request": {"uri": "me/promos"}, + "response": { + "check": [ {"condition": "json.length === 0"} ] + } + }, + + { + "comment": "root: list all promos available, should be three", + "request": { + "session": "rootSession", + "uri": "promos" + }, + "response": { + "check": [ {"condition": "json.length === 3"} ] + } + }, + + { + "comment": "root: apply AccountCredit5 promotion to account", + "request": { + "uri": "users/test_user_small_promo/promos", + "method": "put", + "entity": { + "name": "AccountCredit5" + } + } + }, + + { + "comment": "root: list promos available, should be credit", + "request": {"uri": "users/test_user_small_promo/promos"}, + "response": { + "check": [ + {"condition": "json.length === 1"}, + {"condition": "json[0].getName() === 'AccountCredit5'"} + ] + } + }, + + { + "comment": "user: list promos available, should be credit", + "request": { + "session": "userSession", + "uri": "me/promos" + }, + "response": { + "check": [ + {"condition": "json.length === 1"}, + {"condition": "json[0].getName() === 'AccountCredit5'"} + ] + } + }, + + { + "comment": "user: try to add second AccountCredit5, admin only", + "request": { + "uri": "users/test_user_small_promo/promos", + "method": "put", + "entity": { + "name": "AccountCredit5" + } + }, + "response": { "status": 403 } + }, + + { + "comment": "root: add second AccountCredit5 promotion to account", + "request": { + "session": "rootSession", + "uri": "users/test_user_small_promo/promos", + "method": "put", + "entity": { + "name": "AccountCredit5" + } + } + }, + + { + "comment": "root: add third AccountCredit5 promotion to account", + "request": { + "uri": "users/test_user_small_promo/promos", + "method": "put", + "entity": { + "name": "AccountCredit5" + } + } + }, + + { + "comment": "root: add fourth AccountCredit5 promotion to account", + "request": { + "uri": "users/test_user_small_promo/promos", + "method": "put", + "entity": { + "name": "AccountCredit5" + } + }, + "response": { + "store": "promos" + } + }, + + { + "comment": "root: list promos available, should be four credits", + "request": {"uri": "users/test_user_small_promo/promos"}, + "response": { + "check": [ + {"condition": "json.length === 4"}, + {"condition": "json[0].getName() === 'AccountCredit5'"}, + {"condition": "json[1].getName() === 'AccountCredit5'"}, + {"condition": "json[2].getName() === 'AccountCredit5'"}, + {"condition": "json[3].getName() === 'AccountCredit5'"} + ] + } + }, + + { + "comment": "user: list promos available, should be four credits", + "request": { + "session": "userSession", + "uri": "me/promos" + }, + "response": { + "check": [ + {"condition": "json.length === 4"}, + {"condition": "json[0].getName() === 'AccountCredit5'"}, + {"condition": "json[1].getName() === 'AccountCredit5'"}, + {"condition": "json[2].getName() === 'AccountCredit5'"}, + {"condition": "json[3].getName() === 'AccountCredit5'"} + ] + } + }, + + { + "comment": "root: remove last AccountCredit5 promotion", + "request": { + "session": "rootSession", + "uri": "users/test_user_small_promo/promos/{{promos.[0].uuid}}", + "method": "delete" + }, + "response": { + "check": [ + {"condition": "json.length == 3"} + ] + } + }, + + { + "comment": "user: list promos available, should be three credits", + "request": { + "session": "userSession", + "uri": "me/promos" + }, + "response": { + "check": [ + {"condition": "json.length === 3"}, + {"condition": "json[0].getName() === 'AccountCredit5'"}, + {"condition": "json[1].getName() === 'AccountCredit5'"}, + {"condition": "json[2].getName() === 'AccountCredit5'"} + ] + } + }, + + { + "comment": "get my payment methods, expect three, tokenize a credit card", + "request": { "uri": "me/paymentMethods" }, + "response": { + "store": "paymentMethods", + "check": [ {"condition": "json.length === 3"} ] + }, + "after": "stripe_tokenize_card" + }, + + { + "comment": "add plan, using 'credit' payment method, also applies promotional credits", + "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", + "paymentMethodObject": { + "paymentMethodType": "credit", + "paymentInfo": "{{stripeToken}}" + } + } + }, + "response": { + "store": "accountPlan" + } + }, + + { + "before": "sleep 15s", + "comment": "start the network", + "request": { + "uri": "me/networks/{{accountPlan.network}}/actions/start?cloud=MockCompute®ion=nyc_mock", + "method": "post" + }, + "response": { + "store": "newNetworkNotification" + } + }, + + { + "before": "sleep 10s", + "comment": "verify the network is running", + "request": { "uri": "me/networks/{{accountPlan.network}}" }, + "response": { + "check": [ {"condition": "json.getState().name() == 'running'"} ] + } + }, + + { + "comment": "user: list promos available, should be no credits left", + "request": { + "session": "userSession", + "uri": "me/promos" + }, + "response": { + "check": [ + {"condition": "json.length === 0"} + ] + } + }, + + { + "comment": "user: list all account payment methods, should be five, with all promo credits deleted", + "request": { "uri": "me/paymentMethods?all=true" }, + "response": { + "store": "paymentMethods", + "check": [ + {"condition": "json.length === 5"}, + {"condition": "_find(json, function(p) { return p.getPaymentMethodType().name() === 'credit'; }) !== null"}, + {"condition": "_find(json, function(p) { return p.getPaymentMethodType().name() === 'credit'; }).deleted() === false"}, + {"condition": "_find(json, function(p) { return p.getPaymentMethodType().name() === 'promotional_credit' && p.deleted(); }) !== null"}, + {"condition": "_find(json, function(p) { return p.getPaymentMethodType().name() === 'promotional_credit' && !p.deleted(); }) === null"} + ] + } + }, + + { + "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[0].enabled()"} + ] + } + }, + + { + "comment": "verify account plan payment info", + "request": { "uri": "me/plans/{{accountPlan.uuid}}/paymentMethod" }, + "response": { + "store": "creditPaymentMethod", + "check": [ + {"condition": "json.getPaymentMethodType().name() === 'credit'"}, + {"condition": "json.getMaskedPaymentInfo() == 'XXXX-XXXX-XXXX-4242'"} + ] + } + }, + + { + "comment": "verify bill exists and was 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 via promo credits", + "request": { "uri": "me/payments" }, + "response": { + "check": [ + {"condition": "json.length === 3"}, + {"condition": "json[0].getPlan() === '{{plans.[0].uuid}}'"}, + {"condition": "json[0].getAccountPlan() === '{{accountPlan.uuid}}'"}, + {"condition": "json[1].getAccountPlan() === '{{accountPlan.uuid}}'"}, + {"condition": "json[2].getAccountPlan() === '{{accountPlan.uuid}}'"}, + {"condition": "json[0].getAmount() === 200"}, + {"condition": "json[1].getAmount() === 500"}, + {"condition": "json[2].getAmount() === 500"}, + {"condition": "json[0].getStatus().name() === 'success'"}, + {"condition": "json[1].getStatus().name() === 'success'"}, + {"condition": "json[2].getStatus().name() === 'success'"}, + {"condition": "json[0].getType().name() === 'credit_applied'"}, + {"condition": "json[1].getType().name() === 'credit_applied'"}, + {"condition": "json[2].getType().name() === 'credit_applied'"}, + {"condition": "_find(paymentMethods, function (p) { return p.getUuid() === json[0].getPaymentMethod(); }).getPaymentMethodType().name() === 'promotional_credit'"}, + {"condition": "_find(paymentMethods, function (p) { return p.getUuid() === json[1].getPaymentMethod(); }).getPaymentMethodType().name() === 'promotional_credit'"}, + {"condition": "_find(paymentMethods, function (p) { return p.getUuid() === json[2].getPaymentMethod(); }).getPaymentMethodType().name() === 'promotional_credit'"} + ] + } + }, + + { + "comment": "verify payment exists via plan and is successful via promo credits", + "request": { "uri": "me/plans/{{accountPlan.uuid}}/payments" }, + "response": { + "check": [ + {"condition": "json.length === 3"}, + {"condition": "json[0].getPlan() === '{{plans.[0].uuid}}'"}, + {"condition": "json[0].getAccountPlan() === '{{accountPlan.uuid}}'"}, + {"condition": "json[1].getAccountPlan() === '{{accountPlan.uuid}}'"}, + {"condition": "json[2].getAccountPlan() === '{{accountPlan.uuid}}'"}, + {"condition": "json[0].getAmount() === 200"}, + {"condition": "json[1].getAmount() === 500"}, + {"condition": "json[2].getAmount() === 500"}, + {"condition": "json[0].getStatus().name() === 'success'"}, + {"condition": "json[1].getStatus().name() === 'success'"}, + {"condition": "json[2].getStatus().name() === 'success'"}, + {"condition": "json[0].getType().name() === 'credit_applied'"}, + {"condition": "json[1].getType().name() === 'credit_applied'"}, + {"condition": "json[2].getType().name() === 'credit_applied'"}, + {"condition": "_find(paymentMethods, function (p) { return p.getUuid() === json[0].getPaymentMethod(); }).getPaymentMethodType().name() === 'promotional_credit'"}, + {"condition": "_find(paymentMethods, function (p) { return p.getUuid() === json[1].getPaymentMethod(); }).getPaymentMethodType().name() === 'promotional_credit'"}, + {"condition": "_find(paymentMethods, function (p) { return p.getUuid() === json[2].getPaymentMethod(); }).getPaymentMethodType().name() === 'promotional_credit'"}, + {"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'"} + ] + } + }, + + { + "before": "fast_forward_and_bill 31d 30s", + "comment": "fast-forward +31 days, verify a new bill exists for first accountPlan", + "request": { "uri": "me/plans/{{accountPlan.uuid}}/bills" }, + "response": { + "check": [ + {"condition": "json.length === 2"}, + {"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 a successful payment for accountPlan has been made via credit card", + "request": { "uri": "me/plans/{{accountPlan.uuid}}/payments" }, + "response": { + "check": [ + {"condition": "json.length === 4"}, + {"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'"}, + {"condition": "json[0].getType().name() === 'payment'"}, + {"condition": "json[0].getPaymentMethod() === _find(paymentMethods, function(p) { return p.getPaymentMethodType().name() === 'credit'; }).getUuid()"}, + {"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'"} + ] + } + }, + + { + "comment": "Verify a successful payment for accountPlan has been made via credit card", + "request": { "uri": "me/plans/{{accountPlan.uuid}}/payments" }, + "response": { + "check": [ + {"condition": "json.length === 4"}, + {"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'"}, + {"condition": "json[0].getType().name() === 'payment'"}, + {"condition": "json[0].getPaymentMethod() === _find(paymentMethods, function(p) { return p.getPaymentMethodType().name() === 'credit'; }).getUuid()"}, + {"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'"} + ] + } + }, + + { + "comment": "root: apply another AccountCredit5 promotion to account", + "request": { + "session": "rootSession", + "uri": "users/test_user_small_promo/promos", + "method": "put", + "entity": { + "name": "AccountCredit5" + } + } + }, + + { + "comment": "user: list active account payment methods, should be two, one credit card and one unused promotional credit", + "request": { + "session": "userSession", + "uri": "me/paymentMethods" + }, + "response": { + "store": "paymentMethods", + "check": [ + {"condition": "json.length === 2"}, + {"condition": "_find(json, function(p) { return p.getPaymentMethodType().name() === 'credit'; }).deleted() === false"}, + {"condition": "_find(json, function(p) { return p.getPaymentMethodType().name() === 'promotional_credit'; }).deleted() === false"} + ] + } + }, + + { + "before": "fast_forward_and_bill 31d 30s", + "comment": "fast-forward +31 days, verify a new bill exists for first accountPlan", + "request": { "uri": "me/plans/{{accountPlan.uuid}}/bills" }, + "response": { + "check": [ + {"condition": "json.length === 3"}, + {"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 a successful payment for accountPlan has been made partially via promo credit and partially via credit card", + "request": { "uri": "me/plans/{{accountPlan.uuid}}/payments" }, + "response": { + "check": [ + {"condition": "json.length === 6"}, + {"condition": "json[0].getPlan() === '{{plans.[0].uuid}}'"}, + {"condition": "json[0].getAccountPlan() === '{{accountPlan.uuid}}'"}, + {"condition": "json[0].getAmount() === 700"}, + {"condition": "json[1].getAmount() === 500"}, + {"condition": "json[0].getStatus().name() === 'success'"}, + {"condition": "json[0].getType().name() === 'payment'"}, + {"condition": "json[1].getType().name() === 'credit_applied'"}, + {"condition": "json[0].getPaymentMethod() === _find(paymentMethods, function(p) { return p.getPaymentMethodType().name() === 'credit'; }).getUuid()"}, + {"condition": "json[1].getPaymentMethod() === _find(paymentMethods, function(p) { return p.getPaymentMethodType().name() === 'promotional_credit'; }).getUuid()"}, + {"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'"} + ] + } + } +] \ No newline at end of file diff --git a/pom.xml b/pom.xml index 18441186..d08d5741 100644 --- a/pom.xml +++ b/pom.xml @@ -66,18 +66,19 @@ This code is available under the GNU Affero General Public License, version 3: h -Xmx1024m -XX:MaxPermSize=256m bubble.test.DbInit - bubble.test.AuthTest - bubble.test.PaymentTest - bubble.test.RecurringBillingTest - bubble.test.FirstMonthFreePromotionTest - bubble.test.ReferralMonthFreePromotionTest - bubble.test.FirstMonthAndReferralMonthPromotionTest - bubble.test.DriverTest - bubble.test.ProxyTest - bubble.test.TrafficAnalyticsTest - bubble.test.BackupTest - bubble.test.NetworkTest - bubble.rule.bblock.spec.BlockListTest + bubble.test.system.AuthTest + bubble.test.payment.PaymentTest + bubble.test.payment.RecurringBillingTest + bubble.test.promo.FirstMonthFreePromotionTest + bubble.test.promo.ReferralMonthFreePromotionTest + bubble.test.promo.AccountCreditTest + bubble.test.promo.FirstMonthAndReferralMonthPromotionTest + bubble.test.system.DriverTest + bubble.test.filter.ProxyTest + bubble.test.filter.TrafficAnalyticsTest + bubble.test.system.BackupTest + bubble.test.system.NetworkTest + bubble.abp.spec.BlockListTest