소스 검색

add support for account credit, partial payments

tags/v0.7.2
Jonathan Cobb 5 년 전
부모
커밋
28786462aa
59개의 변경된 파일1080개의 추가작업 그리고 143개의 파일을 삭제
  1. +1
    -0
      bubble-server/src/main/java/bubble/ApiConstants.java
  2. +49
    -41
      bubble-server/src/main/java/bubble/cloud/payment/PaymentDriverBase.java
  3. +1
    -1
      bubble-server/src/main/java/bubble/cloud/payment/PaymentServiceDriver.java
  4. +3
    -5
      bubble-server/src/main/java/bubble/cloud/payment/delegate/DelegatedPaymentDriver.java
  5. +12
    -1
      bubble-server/src/main/java/bubble/cloud/payment/promo/PromotionalPaymentDriverBase.java
  6. +7
    -4
      bubble-server/src/main/java/bubble/cloud/payment/promo/PromotionalPaymentServiceDriver.java
  7. +13
    -0
      bubble-server/src/main/java/bubble/cloud/payment/promo/accountCredit/AccountCreditPaymentConfig.java
  8. +43
    -0
      bubble-server/src/main/java/bubble/cloud/payment/promo/accountCredit/AccountCreditPaymentDriver.java
  9. +1
    -2
      bubble-server/src/main/java/bubble/cloud/payment/promo/firstMonthFree/FirstMonthFreePaymentDriver.java
  10. +7
    -3
      bubble-server/src/main/java/bubble/cloud/payment/promo/referralMonthFree/ReferralMonthFreePaymentDriver.java
  11. +48
    -13
      bubble-server/src/main/java/bubble/cloud/payment/stripe/StripePaymentDriver.java
  12. +22
    -0
      bubble-server/src/main/java/bubble/dao/bill/AccountPaymentDAO.java
  13. +1
    -1
      bubble-server/src/main/java/bubble/dao/bill/AccountPlanDAO.java
  14. +9
    -2
      bubble-server/src/main/java/bubble/dao/bill/BillDAO.java
  15. +3
    -2
      bubble-server/src/main/java/bubble/dao/bill/PromotionDAO.java
  16. +1
    -1
      bubble-server/src/main/java/bubble/model/bill/BillStatus.java
  17. +32
    -6
      bubble-server/src/main/java/bubble/model/bill/Promotion.java
  18. +1
    -1
      bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_authorize.java
  19. +53
    -0
      bubble-server/src/main/java/bubble/resources/account/AccountPromotionsResource.java
  20. +9
    -1
      bubble-server/src/main/java/bubble/resources/account/AccountsResource.java
  21. +7
    -0
      bubble-server/src/main/java/bubble/resources/account/MeResource.java
  22. +9
    -5
      bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java
  23. +4
    -1
      bubble-server/src/main/java/bubble/resources/bill/PromotionsResource.java
  24. +2
    -1
      bubble-server/src/main/java/bubble/service/bill/BillingService.java
  25. +55
    -5
      bubble-server/src/main/java/bubble/service/bill/PromotionService.java
  26. +1
    -0
      bubble-server/src/main/resources/message_templates/en_US/server/post_auth/ResourceMessages.properties
  27. +2
    -2
      bubble-server/src/test/java/bubble/mock/MockStripePaymentDriver.java
  28. +2
    -1
      bubble-server/src/test/java/bubble/test/filter/ProxyTest.java
  29. +2
    -1
      bubble-server/src/test/java/bubble/test/filter/TrafficAnalyticsTest.java
  30. +1
    -1
      bubble-server/src/test/java/bubble/test/live/GoDaddyDnsTest.java
  31. +1
    -1
      bubble-server/src/test/java/bubble/test/live/LiveTestBase.java
  32. +1
    -1
      bubble-server/src/test/java/bubble/test/live/Route53DnsTest.java
  33. +1
    -1
      bubble-server/src/test/java/bubble/test/live/S3StorageTest.java
  34. +1
    -1
      bubble-server/src/test/java/bubble/test/payment/PaymentTest.java
  35. +2
    -1
      bubble-server/src/test/java/bubble/test/payment/PaymentTestBase.java
  36. +1
    -1
      bubble-server/src/test/java/bubble/test/payment/RecurringBillingTest.java
  37. +12
    -0
      bubble-server/src/test/java/bubble/test/promo/AccountCreditTest.java
  38. +2
    -1
      bubble-server/src/test/java/bubble/test/promo/FirstMonthAndReferralMonthPromotionTest.java
  39. +3
    -2
      bubble-server/src/test/java/bubble/test/promo/FirstMonthFreePromotionTest.java
  40. +3
    -2
      bubble-server/src/test/java/bubble/test/promo/ReferralMonthFreePromotionTest.java
  41. +2
    -1
      bubble-server/src/test/java/bubble/test/system/AuthTest.java
  42. +1
    -1
      bubble-server/src/test/java/bubble/test/system/BackupTest.java
  43. +2
    -1
      bubble-server/src/test/java/bubble/test/system/DriverTest.java
  44. +1
    -2
      bubble-server/src/test/java/bubble/test/system/LiveNetworkTest.java
  45. +1
    -1
      bubble-server/src/test/java/bubble/test/system/NetworkTest.java
  46. +3
    -1
      bubble-server/src/test/java/bubble/test/system/NetworkTestBase.java
  47. +0
    -5
      bubble-server/src/test/resources/models/manifest-1mo-promo.json
  48. +0
    -5
      bubble-server/src/test/resources/models/manifest-referral-promo.json
  49. +1
    -1
      bubble-server/src/test/resources/models/promo/1mo/cloudService_1mo.json
  50. +5
    -0
      bubble-server/src/test/resources/models/promo/1mo/manifest_1mo.json
  51. +0
    -0
      bubble-server/src/test/resources/models/promo/1mo/promotion_1mo.json
  52. +32
    -0
      bubble-server/src/test/resources/models/promo/credit/cloudService_credit.json
  53. +5
    -0
      bubble-server/src/test/resources/models/promo/credit/manifest_credit.json
  54. +26
    -0
      bubble-server/src/test/resources/models/promo/credit/promotion_credit.json
  55. +1
    -1
      bubble-server/src/test/resources/models/promo/referral/cloudService_referral.json
  56. +5
    -0
      bubble-server/src/test/resources/models/promo/referral/manifest_referral.json
  57. +0
    -0
      bubble-server/src/test/resources/models/promo/referral/promotion_referral.json
  58. +554
    -0
      bubble-server/src/test/resources/models/tests/promo/account_credit.json
  59. +13
    -12
      pom.xml

+ 1
- 0
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";


+ 49
- 41
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<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 {


+ 1
- 1
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);



+ 3
- 5
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);
}


+ 12
- 1
bubble-server/src/main/java/bubble/cloud/payment/promo/PromotionalPaymentDriverBase.java 파일 보기

@@ -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());
}
}


+ 7
- 4
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<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();
}

}

+ 13
- 0
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; }

}

+ 43
- 0
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<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);
}

}

+ 1
- 2
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<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) {


+ 7
- 3
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<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) {


+ 48
- 13
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<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;
}
}

+ 22
- 0
bubble-server/src/main/java/bubble/dao/bill/AccountPaymentDAO.java 파일 보기

@@ -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,


+ 1
- 1
bubble-server/src/main/java/bubble/dao/bill/AccountPlanDAO.java 파일 보기

@@ -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


+ 9
- 2
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<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) {


+ 3
- 2
bubble-server/src/main/java/bubble/dao/bill/PromotionDAO.java 파일 보기

@@ -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))))));
}
}


+ 1
- 1
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); }



+ 32
- 6
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<? 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; }

}

+ 1
- 1
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);
}

}

+ 53
- 0
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)));
}

}

+ 9
- 1
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());


+ 7
- 0
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)


+ 9
- 5
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<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);
}
}
}
}


+ 4
- 1
bubble-server/src/main/java/bubble/resources/bill/PromotionsResource.java 파일 보기

@@ -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}")


+ 2
- 1
bubble-server/src/main/java/bubble/service/bill/BillingService.java 파일 보기

@@ -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());
}
}


+ 55
- 5
bubble-server/src/main/java/bubble/service/bill/PromotionService.java 파일 보기

@@ -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());
}
}

+ 1
- 0
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


+ 2
- 2
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);
}
}



bubble-server/src/test/java/bubble/test/ProxyTest.java → 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;

bubble-server/src/test/java/bubble/test/TrafficAnalyticsTest.java → 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 {

+ 1
- 1
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 {


+ 1
- 1
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;


+ 1
- 1
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;


+ 1
- 1
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;


bubble-server/src/test/java/bubble/test/PaymentTest.java → 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;

bubble-server/src/test/java/bubble/test/PaymentTestBase.java → 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 {

bubble-server/src/test/java/bubble/test/RecurringBillingTest.java → 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;


+ 12
- 0
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"); }

}

bubble-server/src/test/java/bubble/test/FirstMonthAndReferralMonthPromotionTest.java → 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;


bubble-server/src/test/java/bubble/test/FirstMonthFreePromotionTest.java → 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"); }


bubble-server/src/test/java/bubble/test/ReferralMonthFreePromotionTest.java → 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"); }


bubble-server/src/test/java/bubble/test/AuthTest.java → 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;

bubble-server/src/test/java/bubble/test/BackupTest.java → 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;


bubble-server/src/test/java/bubble/test/DriverTest.java → 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 {

bubble-server/src/test/java/bubble/test/LiveNetworkTest.java → 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 {

bubble-server/src/test/java/bubble/test/NetworkTest.java → 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;

bubble-server/src/test/java/bubble/test/NetworkTestBase.java → 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 {


+ 0
- 5
bubble-server/src/test/resources/models/manifest-1mo-promo.json 파일 보기

@@ -1,5 +0,0 @@
[
"manifest-test",
"system/cloudService_1mo_free",
"system/promotion_1mo_free"
]

+ 0
- 5
bubble-server/src/test/resources/models/manifest-referral-promo.json 파일 보기

@@ -1,5 +0,0 @@
[
"manifest-test",
"system/cloudService_referral_free",
"system/promotion_referral_free"
]

bubble-server/src/test/resources/models/system/cloudService_1mo_free.json → 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
}
]

+ 5
- 0
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"
]

bubble-server/src/test/resources/models/system/promotion_1mo_free.json → bubble-server/src/test/resources/models/promo/1mo/promotion_1mo.json 파일 보기


+ 32
- 0
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
}
]

+ 5
- 0
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"
]

+ 26
- 0
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
}
]

bubble-server/src/test/resources/models/system/cloudService_referral_free.json → 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
}
]

+ 5
- 0
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"
]

bubble-server/src/test/resources/models/system/promotion_referral_free.json → bubble-server/src/test/resources/models/promo/referral/promotion_referral.json 파일 보기


+ 554
- 0
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&region=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'"}
]
}
}
]

+ 13
- 12
pom.xml 파일 보기

@@ -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>


불러오는 중...
취소
저장