@@ -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"; | |||
@@ -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<T> extends CloudServiceDriverBase<T> 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<T> extends CloudServiceDriverBase<T> 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<AccountPayment> 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<T> extends CloudServiceDriverBase<T> 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<AccountPayment> 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<AccountPaymentMethod> accountPaymentMethods = paymentMethodDAO.findByAccountAndPromoAndNotDeleted(accountPlan.getAccount()); | |||
final Map<Promotion, AccountPaymentMethod> 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<AccountPaymentMethod> accountPaymentMethods = paymentMethodDAO.findByAccountAndPromoAndNotDeleted(accountPlan.getAccount()); | |||
final List<Promotion> 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<T> extends CloudServiceDriverBase<T> 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<Bill> unpaidBills = billDAO.findUnpaidByAccountPlan(accountPlan.getUuid()); | |||
final List<Bill> unpaidBills = billDAO.findUnpaidByAccountPlan(accountPlanUuid); | |||
if (unpaidBills.isEmpty()) { | |||
accountPlanDAO.update(accountPlan.setEnabled(true)); | |||
} else { | |||
@@ -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); | |||
@@ -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); | |||
} | |||
@@ -28,6 +28,13 @@ public abstract class PromotionalPaymentDriverBase<T> 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<T> 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()); | |||
} | |||
} | |||
@@ -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<Promotion, AccountPaymentMethod> promos, | |||
List<Promotion> promos, | |||
Set<Promotion> 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(); | |||
} | |||
} |
@@ -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; } | |||
} |
@@ -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<AccountCreditPaymentConfig> { | |||
@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<Promotion> promos, | |||
Set<Promotion> 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); | |||
} | |||
} |
@@ -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<Pr | |||
@Override public boolean canUseNow(Bill bill, | |||
Promotion promo, | |||
PromotionalPaymentServiceDriver promoDriver, | |||
Map<Promotion, AccountPaymentMethod> promos, | |||
List<Promotion> promos, | |||
Set<Promotion> usable, | |||
AccountPlan accountPlan, | |||
AccountPaymentMethod paymentMethod) { | |||
@@ -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<Promotion, AccountPaymentMethod> promos, | |||
List<Promotion> promos, | |||
Set<Promotion> 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) { | |||
@@ -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<StripePaymentDriverCo | |||
@Override public boolean authorize(BubblePlan plan, | |||
String accountPlanUuid, | |||
String billUuid, | |||
AccountPaymentMethod paymentMethod) { | |||
final String planUuid = plan.getUuid(); | |||
final String paymentMethodUuid = paymentMethod.getUuid(); | |||
@@ -157,7 +162,21 @@ public class StripePaymentDriver extends PaymentDriverBase<StripePaymentDriverCo | |||
final Long price = plan.getPrice(); | |||
if (price <= 0) throw invalidEx("err.purchase.priceInvalid"); | |||
chargeParams.put("amount", price); // Amount in cents | |||
// less any credits already applied | |||
final int paidSoFar; | |||
if (billUuid == null) { | |||
// first payments on plan, find all credits for plan | |||
paidSoFar = totalPayments(accountPaymentDAO.findByAccountAndAccountPlanAndPaid(paymentMethod.getAccount(), accountPlanUuid)); | |||
} else { | |||
paidSoFar = totalPayments(accountPaymentDAO.findByAccountAndAccountPlanAndBillAndPaid(paymentMethod.getAccount(), accountPlanUuid, billUuid)); | |||
} | |||
if (paidSoFar >= 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<StripePaymentDriverCo | |||
chargeParams.put("capture", false); | |||
final String chargeJson = json(chargeParams, COMPACT_MAPPER); | |||
final String authCacheKey = getAuthCacheKey(accountPlanUuid, paymentMethodUuid); | |||
final String chargeId = authCache.get(authCacheKey); | |||
if (chargeId != null) { | |||
final String authChargeJson = authCache.get(authCacheKey); | |||
if (authChargeJson != null) { | |||
log.warn("authorize: already authorized: "+authCacheKey); | |||
return true; | |||
} | |||
@@ -184,7 +203,7 @@ public class StripePaymentDriver extends PaymentDriverBase<StripePaymentDriverCo | |||
} else { | |||
log.info("authorize: successful: charge=" + chargeJson); | |||
} | |||
authCache.set(authCacheKey, charge.getId(), EX, AUTH_CACHE_DURATION); | |||
authCache.set(authCacheKey, json(new AuthCharge(charge.getId(), chargeAmount)), EX, AUTH_CACHE_DURATION); | |||
return true; | |||
case "pending": | |||
@@ -229,11 +248,9 @@ public class StripePaymentDriver extends PaymentDriverBase<StripePaymentDriverCo | |||
final Refund refund; | |||
final RedisService refundCache = getRefundCache(); | |||
try { | |||
final Long price = plan.getPrice(); | |||
if (price <= 0) throw invalidEx("err.purchase.priceInvalid"); | |||
final String chargeId = authCache.get(authCacheKey); | |||
if (chargeId == null) throw invalidEx("err.purchase.authNotFound"); | |||
final AuthCharge authCharge = getAuthCharge(authCache, authCacheKey); | |||
final String chargeId = authCharge.getChargeId(); | |||
final long amount = authCharge.getAmount(); | |||
final String refunded = refundCache.get(chargeId); | |||
if (refunded != null) { | |||
@@ -243,7 +260,7 @@ public class StripePaymentDriver extends PaymentDriverBase<StripePaymentDriverCo | |||
} | |||
refundParams.put("charge", chargeId); | |||
refundParams.put("amount", price); | |||
refundParams.put("amount", amount); | |||
refund = Refund.create(refundParams); | |||
if (refund.getStatus() == null) { | |||
final String msg = "cancelAuthorization: no status returned for Charge, accountPlan=" + accountPlanUuid + " with paymentMethod=" + paymentMethodUuid; | |||
@@ -253,8 +270,9 @@ public class StripePaymentDriver extends PaymentDriverBase<StripePaymentDriverCo | |||
final String msg; | |||
switch (refund.getStatus()) { | |||
case "succeeded": | |||
log.info("cancelAuthorization: authorization of "+price+" successful cancelled"); | |||
log.info("cancelAuthorization: authorization of "+amount+" successful cancelled"); | |||
refundCache.set(chargeId, refund.getId(), EX, REFUND_CACHE_DURATION); | |||
authCache.del(authCacheKey); | |||
return true; | |||
case "pending": | |||
@@ -312,8 +330,13 @@ public class StripePaymentDriver extends PaymentDriverBase<StripePaymentDriverCo | |||
return new ChargeResult().setAmountCharged(chargeAmount).setChargeId(charged); | |||
} | |||
final String chargeId = authCache.get(authCacheKey); | |||
if (chargeId == null) throw invalidEx("err.purchase.authNotFound"); | |||
final AuthCharge authCharge = getAuthCharge(authCache, authCacheKey); | |||
final String chargeId = authCharge.getChargeId(); | |||
final long amount = authCharge.getAmount(); | |||
if (amount != chargeAmount) { | |||
// should never happen | |||
throw invalidEx("err.purchase.chargeAmountMismatch"); | |||
} | |||
try { | |||
charge = Charge.retrieve(chargeId); | |||
@@ -375,6 +398,12 @@ public class StripePaymentDriver extends PaymentDriverBase<StripePaymentDriverCo | |||
} | |||
} | |||
private AuthCharge getAuthCharge(RedisService authCache, String authCacheKey) { | |||
final String authChargeJson = authCache.get(authCacheKey); | |||
if (authChargeJson == null) throw invalidEx("err.purchase.authNotFound"); | |||
return json(authChargeJson, AuthCharge.class); | |||
} | |||
@Override protected String refund(AccountPlan accountPlan, | |||
AccountPayment payment, | |||
AccountPaymentMethod paymentMethod, | |||
@@ -437,4 +466,10 @@ public class StripePaymentDriver extends PaymentDriverBase<StripePaymentDriverCo | |||
throw invalidEx("err.refund.unknownError", msg); | |||
} | |||
} | |||
@NoArgsConstructor @AllArgsConstructor | |||
private static class AuthCharge { | |||
@Getter @Setter private String chargeId; | |||
@Getter @Setter private long amount; | |||
} | |||
} |
@@ -9,6 +9,8 @@ import org.springframework.stereotype.Repository; | |||
import java.util.List; | |||
import static org.hibernate.criterion.Restrictions.*; | |||
@Repository | |||
public class AccountPaymentDAO extends AccountOwnedEntityDAO<AccountPayment> { | |||
@@ -27,6 +29,16 @@ public class AccountPaymentDAO extends AccountOwnedEntityDAO<AccountPayment> { | |||
return findByFields("account", accountUuid, "accountPlan", accountPlanUuid); | |||
} | |||
public List<AccountPayment> 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<AccountPayment> findByAccountAndAccountPlanAndBill(String accountUuid, String accountPlanUuid, String billUuid) { | |||
return findByFields("account", accountUuid, "accountPlan", accountPlanUuid, "bill", billUuid); | |||
} | |||
@@ -39,6 +51,16 @@ public class AccountPaymentDAO extends AccountOwnedEntityDAO<AccountPayment> { | |||
"status", AccountPaymentStatus.success); | |||
} | |||
public List<AccountPayment> 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<AccountPayment> findByAccountAndPaymentSuccess(String accountUuid) { | |||
return findByFields("account", accountUuid, | |||
"type", AccountPaymentType.payment, | |||
@@ -93,7 +93,7 @@ public class AccountPlanDAO extends AccountOwnedEntityDAO<AccountPlan> { | |||
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 | |||
@@ -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<Bill> { | |||
@@ -26,11 +28,16 @@ public class BillDAO extends AccountOwnedEntityDAO<Bill> { | |||
} | |||
public List<Bill> 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<Bill> 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) { | |||
@@ -33,12 +33,13 @@ public class PromotionDAO extends AbstractCRUDDAO<Promotion> { | |||
return filterActive(findByFields("enabled", true, "code", null, "referral", false)); | |||
} | |||
public List<Promotion> findEnabledAndActiveWithNoCodeOrWithCode(String code) { | |||
public List<Promotion> 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)))))); | |||
} | |||
} | |||
@@ -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); } | |||
@@ -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<? super Promotion> SORT_PAYMENT_METHOD_CTIME = (Comparator<Promotion>) (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; } | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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))); | |||
} | |||
} |
@@ -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()); | |||
@@ -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) | |||
@@ -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<AccountPlan, Acco | |||
} else { | |||
paymentMethod = request.getPaymentMethodObject(); | |||
} | |||
if (paymentMethod != null) { | |||
paymentMethod.setAccount(caller.getUuid()).validate(errors, configuration); | |||
if (paymentMethod != null && plan != null) { | |||
if (paymentMethod.hasPromotion() || paymentMethod.getPaymentMethodType() == PaymentMethodType.promotional_credit) { | |||
// cannot pay with a promo credit, must supply another payment method. | |||
// promos will be applied at purchase, and may result in no charge to this payment method | |||
errors.addViolation("err.purchase.paymentMethodNotFound"); | |||
} else { | |||
paymentMethod.setAccount(caller.getUuid()).validate(errors, configuration); | |||
} | |||
} | |||
} | |||
} | |||
@@ -37,7 +37,10 @@ public class PromotionsResource { | |||
public Response listPromos(@Context ContainerRequest ctx, | |||
@QueryParam("code") String code) { | |||
final Account caller = optionalUserPrincipal(ctx); | |||
return ok(promotionDAO.findEnabledAndActiveWithNoCodeOrWithCode(code)); | |||
if (caller != null && caller.admin()) { | |||
return ok(promotionDAO.findAll()); | |||
} | |||
return ok(promotionDAO.findVisibleAndEnabledAndActiveWithNoCodeOrWithCode(code)); | |||
} | |||
@GET @Path("/{id}") | |||
@@ -24,6 +24,7 @@ import org.joda.time.Days; | |||
import org.springframework.beans.factory.annotation.Autowired; | |||
import org.springframework.stereotype.Service; | |||
import java.util.Collections; | |||
import java.util.List; | |||
import static java.util.concurrent.TimeUnit.HOURS; | |||
@@ -167,7 +168,7 @@ public class BillingService extends SimpleDaemon { | |||
final PaymentServiceDriver paymentDriver = paymentService.getPaymentDriver(configuration); | |||
for (Bill bill : bills) { | |||
if (paymentDriver.getPaymentMethodType().requiresAuth()) { | |||
if (!paymentDriver.authorize(plan, accountPlan.getUuid(), paymentMethod)) { | |||
if (!paymentDriver.authorize(plan, accountPlan.getUuid(), bill.getUuid(), paymentMethod)) { | |||
return die("payBills: paymentDriver.authorized returned false for accountPlan="+accountPlan.getUuid()+", paymentMethod="+paymentMethod.getUuid()+", bill="+bill.getUuid()); | |||
} | |||
} | |||
@@ -6,6 +6,7 @@ import bubble.cloud.payment.promo.PromotionalPaymentServiceDriver; | |||
import bubble.dao.account.AccountDAO; | |||
import bubble.dao.account.ReferralCodeDAO; | |||
import bubble.dao.bill.AccountPaymentDAO; | |||
import bubble.dao.bill.AccountPaymentMethodDAO; | |||
import bubble.dao.bill.PromotionDAO; | |||
import bubble.dao.cloud.CloudServiceDAO; | |||
import bubble.model.account.Account; | |||
@@ -23,9 +24,11 @@ import java.util.*; | |||
import java.util.stream.Collectors; | |||
import static bubble.model.bill.AccountPayment.totalPayments; | |||
import static bubble.model.bill.Promotion.SORT_PAYMENT_METHOD_CTIME; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.empty; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.shortError; | |||
import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx; | |||
import static org.cobbzilla.util.json.JsonUtil.json; | |||
import static org.cobbzilla.wizard.resources.ResourceUtil.*; | |||
import static org.cobbzilla.wizard.server.RestServerBase.reportError; | |||
@Service @Slf4j | |||
@@ -36,6 +39,7 @@ public class PromotionService { | |||
@Autowired private CloudServiceDAO cloudDAO; | |||
@Autowired private AccountDAO accountDAO; | |||
@Autowired protected AccountPaymentDAO accountPaymentDAO; | |||
@Autowired protected AccountPaymentMethodDAO accountPaymentMethodDAO; | |||
@Autowired private BubbleConfiguration configuration; | |||
public void applyPromotions(Account account, String code) { | |||
@@ -142,7 +146,7 @@ public class PromotionService { | |||
Bill bill, | |||
AccountPaymentMethod paymentMethod, | |||
PaymentServiceDriver paymentDriver, | |||
Map<Promotion, AccountPaymentMethod> promos, | |||
List<Promotion> promos, | |||
long chargeAmount) { | |||
if (chargeAmount <= 0) { | |||
log.error("usePromotions: chargeAmount <= 0 : "+chargeAmount); | |||
@@ -155,8 +159,8 @@ public class PromotionService { | |||
// find the payment cloud associated with the promo, defer to that | |||
final String accountPlanUuid = accountPlan.getUuid(); | |||
for (Promotion promo : promos.keySet()) { | |||
final AccountPaymentMethod apm = promos.get(promo); | |||
for (Promotion promo : promos) { | |||
final AccountPaymentMethod apm = promo.getPaymentMethod(); | |||
final CloudService promoCloud = cloudDAO.findByUuid(promo.getCloud()); | |||
final String prefix = getClass().getSimpleName()+": "; | |||
if (promoCloud == null) { | |||
@@ -202,7 +206,7 @@ public class PromotionService { | |||
final int promoCredits = totalPayments(creditsByThisPromo); | |||
log.info("purchase: promotion applied credits of " + promoCredits + " on a bill of " + bill.getTotal() + ", using current paymentMethod to pay the remainder; reauthorizing now..."); | |||
chargeAmount -= promoCredits; | |||
if (paymentDriver.getPaymentMethodType().requiresAuth() && !paymentDriver.authorize(plan, accountPlanUuid, paymentMethod)) { | |||
if (paymentDriver.getPaymentMethodType().requiresAuth() && !paymentDriver.authorize(plan, accountPlanUuid, bill.getUuid(), paymentMethod)) { | |||
reportError(prefix+"purchase: after applying credit and cancelling previous charge authorization, new charge authorization failed"); | |||
continue; | |||
} | |||
@@ -218,4 +222,50 @@ public class PromotionService { | |||
} | |||
return chargeAmount; | |||
} | |||
public List<Promotion> listPromosForAccount(String accountUuid) { | |||
final List<Promotion> promos = new ArrayList<>(); | |||
final List<AccountPaymentMethod> 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<Promotion> 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<Promotion> 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()); | |||
} | |||
} |
@@ -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 | |||
@@ -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); | |||
} | |||
} | |||
@@ -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; |
@@ -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 { |
@@ -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 { | |||
@@ -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; | |||
@@ -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; | |||
@@ -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; | |||
@@ -1,4 +1,4 @@ | |||
package bubble.test; | |||
package bubble.test.payment; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.junit.Test; |
@@ -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 { |
@@ -1,4 +1,4 @@ | |||
package bubble.test; | |||
package bubble.test.payment; | |||
import org.junit.Test; | |||
@@ -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"); } | |||
} |
@@ -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; | |||
@@ -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"); } | |||
@@ -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"); } | |||
@@ -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; |
@@ -1,4 +1,4 @@ | |||
package bubble.test; | |||
package bubble.test.system; | |||
import org.junit.Test; | |||
@@ -1,5 +1,6 @@ | |||
package bubble.test; | |||
package bubble.test.system; | |||
import bubble.test.ActivatedBubbleModelTestBase; | |||
import org.junit.Test; | |||
public class DriverTest extends ActivatedBubbleModelTestBase { |
@@ -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 { |
@@ -1,4 +1,4 @@ | |||
package bubble.test; | |||
package bubble.test.system; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.junit.Test; |
@@ -1,4 +1,6 @@ | |||
package bubble.test; | |||
package bubble.test.system; | |||
import bubble.test.ActivatedBubbleModelTestBase; | |||
public class NetworkTestBase extends ActivatedBubbleModelTestBase { | |||
@@ -1,5 +0,0 @@ | |||
[ | |||
"manifest-test", | |||
"system/cloudService_1mo_free", | |||
"system/promotion_1mo_free" | |||
] |
@@ -1,5 +0,0 @@ | |||
[ | |||
"manifest-test", | |||
"system/cloudService_referral_free", | |||
"system/promotion_referral_free" | |||
] |
@@ -5,6 +5,6 @@ | |||
"driverClass": "bubble.cloud.payment.promo.firstMonthFree.FirstMonthFreePaymentDriver", | |||
"driverConfig": {}, | |||
"credentials": {}, | |||
"template": true | |||
"template": false | |||
} | |||
] |
@@ -0,0 +1,5 @@ | |||
[ | |||
"manifest-test", | |||
"promo/1mo/cloudService_1mo", | |||
"promo/1mo/promotion_1mo" | |||
] |
@@ -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 | |||
} | |||
] |
@@ -0,0 +1,5 @@ | |||
[ | |||
"manifest-test", | |||
"promo/credit/cloudService_credit", | |||
"promo/credit/promotion_credit" | |||
] |
@@ -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 | |||
} | |||
] |
@@ -5,6 +5,6 @@ | |||
"driverClass": "bubble.cloud.payment.promo.referralMonthFree.ReferralMonthFreePaymentDriver", | |||
"driverConfig": {}, | |||
"credentials": {}, | |||
"template": true | |||
"template": false | |||
} | |||
] |
@@ -0,0 +1,5 @@ | |||
[ | |||
"manifest-test", | |||
"promo/referral/cloudService_referral", | |||
"promo/referral/promotion_referral" | |||
] |
@@ -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'"} | |||
] | |||
} | |||
} | |||
] |
@@ -66,18 +66,19 @@ This code is available under the GNU Affero General Public License, version 3: h | |||
<argLine>-Xmx1024m -XX:MaxPermSize=256m</argLine> | |||
<includes> | |||
<include>bubble.test.DbInit</include> | |||
<include>bubble.test.AuthTest</include> | |||
<include>bubble.test.PaymentTest</include> | |||
<include>bubble.test.RecurringBillingTest</include> | |||
<include>bubble.test.FirstMonthFreePromotionTest</include> | |||
<include>bubble.test.ReferralMonthFreePromotionTest</include> | |||
<include>bubble.test.FirstMonthAndReferralMonthPromotionTest</include> | |||
<include>bubble.test.DriverTest</include> | |||
<include>bubble.test.ProxyTest</include> | |||
<include>bubble.test.TrafficAnalyticsTest</include> | |||
<include>bubble.test.BackupTest</include> | |||
<include>bubble.test.NetworkTest</include> | |||
<include>bubble.rule.bblock.spec.BlockListTest</include> | |||
<include>bubble.test.system.AuthTest</include> | |||
<include>bubble.test.payment.PaymentTest</include> | |||
<include>bubble.test.payment.RecurringBillingTest</include> | |||
<include>bubble.test.promo.FirstMonthFreePromotionTest</include> | |||
<include>bubble.test.promo.ReferralMonthFreePromotionTest</include> | |||
<include>bubble.test.promo.AccountCreditTest</include> | |||
<include>bubble.test.promo.FirstMonthAndReferralMonthPromotionTest</include> | |||
<include>bubble.test.system.DriverTest</include> | |||
<include>bubble.test.filter.ProxyTest</include> | |||
<include>bubble.test.filter.TrafficAnalyticsTest</include> | |||
<include>bubble.test.system.BackupTest</include> | |||
<include>bubble.test.system.NetworkTest</include> | |||
<include>bubble.abp.spec.BlockListTest</include> | |||
</includes> | |||
</configuration> | |||
</plugin> | |||