Преглед изворни кода

introduce ReferralCode and PromotionService. apply promos at registration only (for now)

tags/v0.7.2
Jonathan Cobb пре 4 година
родитељ
комит
d730cf43a1
26 измењених фајлова са 616 додато и 179 уклоњено
  1. +1
    -0
      bubble-server/src/main/java/bubble/ApiConstants.java
  2. +4
    -4
      bubble-server/src/main/java/bubble/cloud/payment/firstMonthFree/FirstMonthFreePaymentDriver.java
  3. +65
    -20
      bubble-server/src/main/java/bubble/cloud/payment/referralMonthFree/ReferralMonthFreePaymentDriver.java
  4. +9
    -1
      bubble-server/src/main/java/bubble/dao/account/AccountDAO.java
  5. +13
    -0
      bubble-server/src/main/java/bubble/dao/account/ReferralCodeDAO.java
  6. +9
    -8
      bubble-server/src/main/java/bubble/dao/bill/PromotionDAO.java
  7. +3
    -1
      bubble-server/src/main/java/bubble/model/account/Account.java
  8. +2
    -0
      bubble-server/src/main/java/bubble/model/account/AccountRegistration.java
  9. +55
    -0
      bubble-server/src/main/java/bubble/model/account/ReferralCode.java
  10. +12
    -24
      bubble-server/src/main/java/bubble/model/bill/AccountPlan.java
  11. +7
    -0
      bubble-server/src/main/java/bubble/resources/account/AccountsResource.java
  12. +20
    -1
      bubble-server/src/main/java/bubble/resources/account/AuthResource.java
  13. +6
    -0
      bubble-server/src/main/java/bubble/resources/account/MeResource.java
  14. +48
    -0
      bubble-server/src/main/java/bubble/resources/account/ReferralCodesResource.java
  15. +3
    -85
      bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java
  16. +135
    -0
      bubble-server/src/main/java/bubble/service/bill/PromotionService.java
  17. +9
    -10
      bubble-server/src/main/java/bubble/service/dbfilter/FilteredEntityIterator.java
  18. +1
    -0
      bubble-server/src/main/resources/message_templates/en_US/server/pre_auth/ResourceMessages.properties
  19. +4
    -1
      bubble-server/src/test/java/bubble/test/BubbleApiRunnerListener.java
  20. +1
    -3
      bubble-server/src/test/java/bubble/test/FirstMonthFreePromotionTest.java
  21. +3
    -3
      bubble-server/src/test/java/bubble/test/ReferralMonthFreePromotionTest.java
  22. +5
    -0
      bubble-server/src/test/resources/models/manifest-referral-promo.json
  23. +10
    -0
      bubble-server/src/test/resources/models/system/cloudService_referral_free.json
  24. +10
    -0
      bubble-server/src/test/resources/models/system/promotion_referral_free.json
  25. +5
    -17
      bubble-server/src/test/resources/models/tests/promo/first_month_free.json
  26. +176
    -1
      bubble-server/src/test/resources/models/tests/promo/referral_month_free.json

+ 1
- 0
bubble-server/src/main/java/bubble/ApiConstants.java Прегледај датотеку

@@ -157,6 +157,7 @@ public class ApiConstants {
public static final String EP_ROLES = ROLES_ENDPOINT;
public static final String EP_SENT_NOTIFICATIONS = "/notifications/outbox";
public static final String EP_RECEIVED_NOTIFICATIONS = "/notifications/inbox";
public static final String EP_REFERRAL_CODES = "/referralCodes";
public static final String EP_STORAGE = "/storage";
public static final String EP_DNS = "/dns";
public static final String EP_BACKUPS = "/backups";


+ 4
- 4
bubble-server/src/main/java/bubble/cloud/payment/firstMonthFree/FirstMonthFreePaymentDriver.java Прегледај датотеку

@@ -10,6 +10,7 @@ import lombok.extern.slf4j.Slf4j;
import java.util.List;

import static org.cobbzilla.util.daemon.ZillaRuntime.empty;
import static org.cobbzilla.wizard.server.RestServerBase.reportError;

@Slf4j
public class FirstMonthFreePaymentDriver extends PaymentDriverBase<FirstMonthPaymentConfig> implements PromotionalPaymentServiceDriver {
@@ -19,7 +20,7 @@ public class FirstMonthFreePaymentDriver extends PaymentDriverBase<FirstMonthPay
@Override public PaymentMethodType getPaymentMethodType() { return PaymentMethodType.promotional_credit; }

@Override public boolean applyPromo(Promotion promo, Account caller) {
// does the caller have exactly one Bill?
// caller must not have any bills
final int billCount = billDAO.countByAccount(caller.getUuid());
if (billCount != 0) {
log.warn("applyPromo: promo="+promo.getName()+", account="+caller.getName()+", account must have no Bills, found "+billCount+" bills");
@@ -37,8 +38,7 @@ public class FirstMonthFreePaymentDriver extends PaymentDriverBase<FirstMonthPay
.setPaymentMethodType(PaymentMethodType.promotional_credit)
.setPaymentInfo(promo.getName())
.setMaskedPaymentInfo(promo.getName())
.setPromotion(promo.getUuid())
);
.setPromotion(promo.getUuid()));
return true;
}

@@ -52,7 +52,6 @@ public class FirstMonthFreePaymentDriver extends PaymentDriverBase<FirstMonthPay
return new PaymentValidationResult("err.paymentMethodType.mismatch");
}
return new PaymentValidationResult(paymentMethod);

}

@Override protected String charge(BubblePlan plan,
@@ -67,6 +66,7 @@ public class FirstMonthFreePaymentDriver extends PaymentDriverBase<FirstMonthPay
}

@Override protected String refund(AccountPlan accountPlan, AccountPayment payment, AccountPaymentMethod paymentMethod, Bill bill, long refundAmount) {
reportError(getClass().getSimpleName()+": refund: cannot issue, ignoring");
return FIRST_MONTH_FREE_INFO;
}



+ 65
- 20
bubble-server/src/main/java/bubble/cloud/payment/referralMonthFree/ReferralMonthFreePaymentDriver.java Прегледај датотеку

@@ -5,30 +5,76 @@ import bubble.cloud.payment.PromotionalPaymentServiceDriver;
import bubble.model.account.Account;
import bubble.model.bill.*;
import bubble.notify.payment.PaymentValidationResult;
import lombok.extern.slf4j.Slf4j;

import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx;
import java.util.List;

import static org.cobbzilla.util.daemon.ZillaRuntime.empty;
import static org.cobbzilla.wizard.server.RestServerBase.reportError;

@Slf4j
public class ReferralMonthFreePaymentDriver extends PaymentDriverBase<ReferralMonthPaymentConfig> implements PromotionalPaymentServiceDriver {

private static final String REFERRAL_MONTH_FREE_INFO = "referralMonthFree";

@Override public PaymentMethodType getPaymentMethodType() { return PaymentMethodType.promotional_credit; }

@Override public boolean applyPromo(Promotion promo, Account caller) { return false; }

@Override public boolean applyReferralPromo(Promotion referralPromo, Account caller, Account referredFrom) {
// todo
// validate referralPromo
// check existing AccountPaymentMethods for caller, they can only have one AccountPaymentMethod of the "joiner" type across all methods
// -- create if not exist
// check existing AccountPaymentMethods for referredFrom, they can only have one AccountPaymentMethod of the "referral" type for the caller
// -- create if not exist
return false;
@Override public boolean applyReferralPromo(Promotion promo, Account caller, Account referredFrom) {
// caller must not have any bills
final int billCount = billDAO.countByAccount(caller.getUuid());
if (billCount != 0) {
log.warn("applyReferralPromo: promo="+promo.getName()+", account="+caller.getName()+", account must have no Bills, found "+billCount+" bills");
return false;
}

// check AccountPaymentMethods for referredFrom
final List<AccountPaymentMethod> referredFromCreditPaymentMethods = paymentMethodDAO.findByAccountAndCloud(referredFrom.getUuid(), promo.getCloud());

// It's OK for the referredFrom user to have many of these, as long as there is not one for this user
for (AccountPaymentMethod apm : referredFromCreditPaymentMethods) {
if (apm.getPaymentInfo().equals(caller.getUuid())) {
log.error("applyReferralPromo: promo="+promo.getName()+", account="+caller.getName()+", referredFrom="+referredFrom.getName()+" has already referred this caller");
return false;
}
}

// does the caller already have one of these?
final List<AccountPaymentMethod> existingCreditPaymentMethods = paymentMethodDAO.findByAccountAndCloud(caller.getUuid(), promo.getCloud());
if (!empty(existingCreditPaymentMethods)) {
log.warn("applyReferralPromo: promo="+promo.getName()+", account="+caller.getName()+", account already has one of these promos applied");
return true; // promo has already been applied, return true
}

// create new APMs for caller and referredFrom
paymentMethodDAO.create(new AccountPaymentMethod()
.setAccount(caller.getUuid())
.setCloud(promo.getCloud())
.setPaymentMethodType(PaymentMethodType.promotional_credit)
.setPaymentInfo(referredFrom.getUuid())
.setMaskedPaymentInfo(promo.getName())
.setPromotion(promo.getUuid()));

paymentMethodDAO.create(new AccountPaymentMethod()
.setAccount(referredFrom.getUuid())
.setCloud(promo.getCloud())
.setPaymentMethodType(PaymentMethodType.promotional_credit)
.setPaymentInfo(caller.getUuid())
.setMaskedPaymentInfo(promo.getName())
.setPromotion(promo.getUuid()));

return true;
}

@Override public PaymentValidationResult validate(AccountPaymentMethod paymentMethod) {
// todo
// validate that this paymentMethod is for this driver
// validate that this paymentMethod has not yet been used on any other AccountPayment
return null;
if (paymentMethod.getPaymentMethodType() != PaymentMethodType.promotional_credit || !paymentMethod.hasPromotion()) {
return new PaymentValidationResult("err.paymentMethodType.mismatch");
}
if (!paymentMethod.getCloud().equals(cloud.getUuid()) || paymentMethod.deleted()) {
return new PaymentValidationResult("err.paymentMethodType.mismatch");
}
return new PaymentValidationResult(paymentMethod);
}

@Override protected String charge(BubblePlan plan,
@@ -36,16 +82,15 @@ public class ReferralMonthFreePaymentDriver extends PaymentDriverBase<ReferralMo
AccountPaymentMethod paymentMethod,
Bill bill,
long chargeAmount) {
// todo
// validate that this paymentMethod is for this driver
// validate that this paymentMethod has not yet been used on any other AccountPayment
// apply the paymentMethod
return null;
// 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 REFERRAL_MONTH_FREE_INFO;
}

@Override protected String refund(AccountPlan accountPlan, AccountPayment payment, AccountPaymentMethod paymentMethod, Bill bill, long refundAmount) {
// cannot refund
throw invalidEx("err.refund.noRefundsForPromotionalCredits");
reportError(getClass().getSimpleName()+": refund: cannot issue, ignoring");
return REFERRAL_MONTH_FREE_INFO;
}

}

+ 9
- 1
bubble-server/src/main/java/bubble/dao/account/AccountDAO.java Прегледај датотеку

@@ -62,6 +62,7 @@ public class AccountDAO extends AbstractCRUDDAO<Account> implements SqlViewSearc
@Autowired private SelfNodeService selfNodeService;
@Autowired private BillDAO billDAO;
@Autowired private SearchService searchService;
@Autowired private ReferralCodeDAO referralCodeDAO;

public Account newAccount(Request req, Account caller, AccountRegistration request, Account parent) {
return create(new Account(request)
@@ -281,12 +282,19 @@ public class AccountDAO extends AbstractCRUDDAO<Account> implements SqlViewSearc
throw invalidEx("err.delete.unpaidBills", "cannot delete account ("+account.getUuid()+") with "+unpaid.size()+" unpaid bills", account.getUuid());
}

final AccountPolicy policy = policyDAO.findSingleByAccount(uuid);
// for referral codes owned by us, set account to null, leave accountUuid in place
final List<ReferralCode> ownedCodes = referralCodeDAO.findByAccount(uuid);
for (ReferralCode c : ownedCodes) referralCodeDAO.update(c.setAccount(null));

// for referral a code we used, set usedBy to null, leave usedByUuid in place
final ReferralCode usedCode = referralCodeDAO.findCodeUsedBy(uuid);
if (usedCode != null) referralCodeDAO.update(usedCode.setUsedBy(null));

log.info("delete ("+Thread.currentThread().getName()+"): starting to delete account-dependent objects");
configuration.deleteDependencies(account);
log.info("delete: finished deleting account-dependent objects");

final AccountPolicy policy = policyDAO.findSingleByAccount(uuid);
switch (policy.getDeletionPolicy()) {
case full_delete:
super.delete(uuid);


+ 13
- 0
bubble-server/src/main/java/bubble/dao/account/ReferralCodeDAO.java Прегледај датотеку

@@ -0,0 +1,13 @@
package bubble.dao.account;

import bubble.model.account.ReferralCode;
import org.springframework.stereotype.Repository;

@Repository
public class ReferralCodeDAO extends AccountOwnedEntityDAO<ReferralCode> {

public ReferralCode findCodeUsedBy(String accountUuid) { return findByUniqueField("usedBy", accountUuid); }

public ReferralCode findByName(String code) { return findByUniqueField("name", code); }

}

+ 9
- 8
bubble-server/src/main/java/bubble/dao/bill/PromotionDAO.java Прегледај датотеку

@@ -25,13 +25,12 @@ public class PromotionDAO extends AbstractCRUDDAO<Promotion> {
return found != null ? found : findByName(id);
}

public Promotion findEnabledWithCode(String code) {
return findByUniqueFields("enabled", true, "code", code);
public Promotion findEnabledAndActiveWithCode(String code) {
return filterActive(findByUniqueFields("enabled", true, "code", code, "referral", false));
}

public List<Promotion> findEnabledAndActiveWithNoCode() {
final List<Promotion> promos = findByFields("enabled", true, "code", null);
return filterActive(promos);
return filterActive(findByFields("enabled", true, "code", null, "referral", false));
}

public List<Promotion> findEnabledAndActiveWithNoCodeOrWithCode(String code) {
@@ -44,12 +43,14 @@ public class PromotionDAO extends AbstractCRUDDAO<Promotion> {
}
}

public List<Promotion> filterActive(List<Promotion> promos) {
return promos.stream().filter(Promotion::active).collect(Collectors.toList());
}

public List<Promotion> findEnabledAndActiveWithReferral() {
return filterActive(findByFields("enabled", true, "referral", true));
}

public Promotion filterActive(Promotion promo) { return promo != null && promo.active() ? promo : null; }

public List<Promotion> filterActive(List<Promotion> promos) {
return promos.stream().filter(Promotion::active).collect(Collectors.toList());
}

}

+ 3
- 1
bubble-server/src/main/java/bubble/model/account/Account.java Прегледај датотеку

@@ -82,7 +82,7 @@ public class Account extends IdentifiableBaseParentEntity implements TokenPrinci

public static final String[] UPDATE_FIELDS = {"url", "description", "autoUpdatePolicy"};
public static final String[] ADMIN_UPDATE_FIELDS = ArrayUtil.append(UPDATE_FIELDS, "suspended", "admin");
public static final String[] CREATE_FIELDS = ArrayUtil.append(ADMIN_UPDATE_FIELDS, "name");
public static final String[] CREATE_FIELDS = ArrayUtil.append(ADMIN_UPDATE_FIELDS, "name", "referralCode");

public static final String ROOT_USERNAME = "root";
public static final int NAME_MIN_LENGTH = 4;
@@ -181,6 +181,8 @@ public class Account extends IdentifiableBaseParentEntity implements TokenPrinci
public static final long INIT_WAIT_INTERVAL = MILLISECONDS.toMillis(250);
public static final long INIT_WAIT_TIMEOUT = SECONDS.toMillis(60);

@Transient @Getter @Setter private transient String promoError;

@Transient @JsonIgnore @Getter @Setter private transient AccountInitializer accountInitializer;
public boolean hasAccountInitializer () { return accountInitializer != null; }



+ 2
- 0
bubble-server/src/main/java/bubble/model/account/AccountRegistration.java Прегледај датотеку

@@ -7,6 +7,8 @@ public class AccountRegistration extends Account {

@Getter @Setter private String password;

@Getter @Setter private String promoCode;

@Getter @Setter private AccountContact contact;
public boolean hasContact () { return contact != null; }



+ 55
- 0
bubble-server/src/main/java/bubble/model/account/ReferralCode.java Прегледај датотеку

@@ -0,0 +1,55 @@
package bubble.model.account;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.cobbzilla.wizard.model.Identifiable;
import org.cobbzilla.wizard.model.IdentifiableBase;
import org.cobbzilla.wizard.model.entityconfig.annotations.*;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Transient;

import static org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric;
import static org.cobbzilla.util.daemon.ZillaRuntime.empty;

@ECType(root=true) @ECTypeUpdate(method="DISABLED")
@Entity @NoArgsConstructor @Accessors(chain=true)
public class ReferralCode extends IdentifiableBase implements HasAccount {

public ReferralCode (ReferralCode other) {
// only the count is initialized, everything else is set manually
setCount(other.getCount());
}

// update is a noop, must update fields manually
@Override public Identifiable update(Identifiable thing) { return this; }

@ECSearchable @ECField(index=10)
@ECForeignKey(entity=Account.class)
@Column(length=UUID_MAXLEN)
@Getter @Setter private String account;

@Column(length=UUID_MAXLEN, nullable=false, updatable=false)
@Getter @Setter private String accountUuid;

@ECSearchable @ECField(index=20)
@ECIndex(unique=true) @Column(nullable=false, updatable=false, length=20)
@Getter @Setter private String name;
public ReferralCode setName () { return setName(randomAlphanumeric(8)); }

@ECSearchable @ECField(index=30)
@ECForeignKey(index=false, entity=Account.class) @ECIndex(unique=true)
@Column(length=UUID_MAXLEN)
@Getter @Setter private String usedBy;

@ECSearchable @ECField(index=40) @ECIndex(unique=true)
@Column(length=UUID_MAXLEN)
@Getter @Setter private String usedByUuid;
public boolean used() { return !empty(usedByUuid); }

@Transient @Getter @Setter private transient int count = 1;

}

+ 12
- 24
bubble-server/src/main/java/bubble/model/bill/AccountPlan.java Прегледај датотеку

@@ -60,79 +60,67 @@ public class AccountPlan extends IdentifiableBase implements HasAccount {
@Column(nullable=false, updatable=false, length=UUID_MAXLEN)
@Getter @Setter private String account;

// refers to an Account.uuid, but we do not use a foreign key, so if the referring Account is deleted
// then a lookup of the referralFrom will return null, and any unused referral promotion cannot be used
@ECSearchable @ECField(index=30)
@Column(length=UUID_MAXLEN, updatable=false)
@Getter @Setter private String referralFrom;
public boolean hasReferralFrom () { return !empty(referralFrom); }

@ECSearchable @ECField(index=40)
@Column(length=100, updatable=false)
@Getter @Setter private String promoCode;
public boolean hasPromoCode () { return !empty(promoCode); }

@ECSearchable @ECField(index=50)
@ECForeignKey(entity=BubblePlan.class)
@Column(nullable=false, updatable=false, length=UUID_MAXLEN)
@Getter @Setter private String plan;

@ECSearchable @ECField(index=60)
@ECSearchable @ECField(index=40)
@ECForeignKey(entity=AccountPaymentMethod.class)
@Column(updatable=false, length=UUID_MAXLEN)
@Getter @Setter private String paymentMethod;

@ECSearchable @ECField(index=70)
@ECSearchable @ECField(index=50)
@ECForeignKey(entity=BubbleDomain.class)
@Column(nullable=false, updatable=false, length=UUID_MAXLEN)
@Getter @Setter private String domain;

@ECSearchable @ECField(index=80)
@ECSearchable @ECField(index=60)
@ECForeignKey(entity=BubbleNetwork.class, index=false) @ECIndex(unique=true)
@Column(length=UUID_MAXLEN)
@Getter @Setter private String network;

@ECSearchable @ECField(index=90)
@ECSearchable @ECField(index=70)
@ECForeignKey(entity=AccountSshKey.class)
@Column(length=UUID_MAXLEN)
@Getter @Setter private String sshKey;
public boolean hasSshKey () { return !empty(sshKey); }

@ECSearchable @ECField(index=100)
@ECSearchable @ECField(index=80)
@Column(nullable=false)
@Getter @Setter private Boolean enabled = false;
public boolean enabled() { return bool(enabled); }
public boolean disabled() { return !enabled(); }

@ECSearchable(type=EntityFieldType.epoch_time) @ECField(index=110)
@ECSearchable(type=EntityFieldType.epoch_time) @ECField(index=90)
@Column(nullable=false)
@ECIndex @Getter @Setter private Long nextBill;

@ECSearchable @ECField(index=120)
@ECSearchable @ECField(index=100)
@Column(nullable=false, length=50)
@Getter @Setter private String nextBillDate;
public AccountPlan setNextBillDate() { return setNextBillDate(BILL_START_END_FORMAT.print(getNextBill())); }

@ECSearchable @ECField(index=130)
@ECSearchable @ECField(index=110)
@ECIndex @Getter @Setter private Long deleted;
public boolean deleted() { return deleted != null; }
public boolean notDeleted() { return !deleted(); }

@ECSearchable @ECField(index=140)
@ECSearchable @ECField(index=120)
@Column(nullable=false)
@ECIndex @Getter @Setter private Boolean closed = false;
public boolean closed() { return bool(closed); }
public boolean notClosed() { return !closed(); }

@ECSearchable @ECField(index=150, type=EntityFieldType.reference)
@ECSearchable @ECField(index=130, type=EntityFieldType.reference)
@ECIndex(unique=true) @Column(length=UUID_MAXLEN)
@Getter @Setter private String deletedNetwork;
public boolean hasDeletedNetwork() { return deletedNetwork != null; }

@ECSearchable @ECField(index=160) @Column(nullable=false)
@ECSearchable @ECField(index=140) @Column(nullable=false)
@Getter @Setter private Boolean refundIssued = false;

@ECSearchable @ECField(index=170, type=EntityFieldType.error)
@ECSearchable @ECField(index=150, type=EntityFieldType.error)
@Getter @Setter private String refundError;

// Fields below are used when creating a new plan, to also create the network associated with it


+ 7
- 0
bubble-server/src/main/java/bubble/resources/account/AccountsResource.java Прегледај датотеку

@@ -560,6 +560,13 @@ public class AccountsResource {
return configuration.subResource(ReceivedNotificationsResource.class, c.account);
}

@Path("/{id}"+EP_REFERRAL_CODES)
public ReferralCodesResource getReferralCodesResource(@Context ContainerRequest ctx,
@PathParam("id") String id) {
final AccountContext c = new AccountContext(ctx, id);
return configuration.subResource(ReferralCodesResource.class, c.account);
}

// Non-admins can only read/edit/delete themselves. Admins can do anything to anyone.
private class AccountContext {
public Account caller;


+ 20
- 1
bubble-server/src/main/java/bubble/resources/account/AuthResource.java Прегледај датотеку

@@ -17,6 +17,7 @@ import bubble.server.BubbleConfiguration;
import bubble.service.account.AuthenticatorService;
import bubble.service.account.StandardAccountMessageService;
import bubble.service.backup.RestoreService;
import bubble.service.bill.PromotionService;
import bubble.service.boot.ActivationService;
import bubble.service.boot.SageHelloService;
import bubble.service.notify.NotificationService;
@@ -25,6 +26,7 @@ import org.cobbzilla.util.collection.NameAndValue;
import org.cobbzilla.wizard.auth.LoginRequest;
import org.cobbzilla.wizard.stream.FileSendableResource;
import org.cobbzilla.wizard.validation.ConstraintViolationBean;
import org.cobbzilla.wizard.validation.SimpleViolationException;
import org.cobbzilla.wizard.validation.ValidationResult;
import org.glassfish.grizzly.http.server.Request;
import org.glassfish.jersey.server.ContainerRequest;
@@ -71,6 +73,7 @@ public class AuthResource {
@Autowired private BubbleNodeDAO nodeDAO;
@Autowired private BubbleConfiguration configuration;
@Autowired private AuthenticatorService authenticatorService;
@Autowired private PromotionService promoService;

public Account updateLastLogin(Account account) { return accountDAO.update(account.setLastLogin()); }

@@ -193,6 +196,11 @@ public class AuthResource {
} else {
request.getContact().validate(errors);
}

if (configuration.paymentsEnabled()) {
errors.addAll(promoService.validatePromotions(request.getPromoCode()));
}

if (errors.isInvalid()) return invalid(errors);

final String parentUuid = thisNetwork.getTag(TAG_PARENT_ACCOUNT, thisNetwork.getAccount());
@@ -200,7 +208,18 @@ public class AuthResource {
if (parent == null) return invalid("err.parent.notFound", "Parent account does not exist: "+parentUuid);

final Account account = accountDAO.newAccount(req, null, request, parent);
return ok(account.waitForAccountInit().setToken(newLoginSession(account)));
SimpleViolationException promoEx = null;
if (configuration.paymentsEnabled()) {
try {
promoService.applyPromotions(account, request.getPromoCode());
} catch (SimpleViolationException e) {
promoEx = e;
}
}
return ok(account
.waitForAccountInit()
.setPromoError(promoEx == null ? null : promoEx.getMessageTemplate())
.setToken(newLoginSession(account)));
}

@POST @Path(EP_LOGIN)


+ 6
- 0
bubble-server/src/main/java/bubble/resources/account/MeResource.java Прегледај датотеку

@@ -336,6 +336,12 @@ public class MeResource {
return configuration.subResource(DevicesResource.class, caller);
}

@Path(EP_REFERRAL_CODES)
public ReferralCodesResource getReferralCodes(@Context ContainerRequest ctx) {
final Account caller = userPrincipal(ctx);
return configuration.subResource(ReferralCodesResource.class, caller);
}

@Autowired private StandardNetworkService networkService;

@GET @Path(EP_STATUS)


+ 48
- 0
bubble-server/src/main/java/bubble/resources/account/ReferralCodesResource.java Прегледај датотеку

@@ -0,0 +1,48 @@
package bubble.resources.account;

import bubble.dao.account.ReferralCodeDAO;
import bubble.model.account.Account;
import bubble.model.account.ReferralCode;
import lombok.extern.slf4j.Slf4j;
import org.glassfish.grizzly.http.server.Request;
import org.glassfish.jersey.server.ContainerRequest;

import java.util.ArrayList;
import java.util.List;

@Slf4j
public class ReferralCodesResource extends AccountOwnedResource<ReferralCode, ReferralCodeDAO> {

public ReferralCodesResource(Account account) { super(account); }

@Override protected boolean canCreate(Request req, ContainerRequest ctx, Account caller, ReferralCode request) {
return caller.admin();
}

@Override protected Object daoCreate(ReferralCode toCreate) {
final List<ReferralCode> createdCodes = new ArrayList<>();
for (int i=0; i<toCreate.getCount(); i++) {
createdCodes.add((ReferralCode) super.daoCreate(new ReferralCode()
.setAccount(toCreate.getAccount())
.setAccountUuid(toCreate.getAccount())
.setUsedBy(null)
.setUsedByUuid(null)
.setName()));
}
return createdCodes;
}

@Override protected boolean canUpdate(ContainerRequest ctx, Account caller, ReferralCode found, ReferralCode request) {
return caller.admin();
}

@Override protected boolean canDelete(ContainerRequest ctx, Account caller, ReferralCode found) {
return caller.admin();
}

@Override protected ReferralCode setReferences(ContainerRequest ctx, Account caller, ReferralCode request) {
request.setAccountUuid(getAccountUuid(ctx));
return super.setReferences(ctx, caller, request);
}

}

+ 3
- 85
bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java Прегледај датотеку

@@ -2,8 +2,6 @@ package bubble.resources.bill;

import bubble.cloud.CloudServiceType;
import bubble.cloud.geoLocation.GeoLocation;
import bubble.cloud.payment.PaymentServiceDriver;
import bubble.cloud.payment.PromotionalPaymentServiceDriver;
import bubble.dao.account.AccountSshKeyDAO;
import bubble.dao.bill.AccountPaymentMethodDAO;
import bubble.dao.bill.AccountPlanDAO;
@@ -15,7 +13,9 @@ import bubble.dao.cloud.BubbleNetworkDAO;
import bubble.dao.cloud.CloudServiceDAO;
import bubble.model.account.Account;
import bubble.model.account.AccountSshKey;
import bubble.model.bill.*;
import bubble.model.bill.AccountPaymentMethod;
import bubble.model.bill.AccountPlan;
import bubble.model.bill.BubblePlan;
import bubble.model.cloud.BubbleDomain;
import bubble.model.cloud.BubbleFootprint;
import bubble.model.cloud.BubbleNetwork;
@@ -41,7 +41,6 @@ import java.util.stream.Collectors;

import static bubble.ApiConstants.*;
import static bubble.model.cloud.BubbleNetwork.validateHostname;
import static org.cobbzilla.util.daemon.ZillaRuntime.empty;
import static org.cobbzilla.util.string.ValidationRegexes.HOST_PATTERN;
import static org.cobbzilla.util.string.ValidationRegexes.validateRegexMatches;
import static org.cobbzilla.wizard.resources.ResourceUtil.*;
@@ -192,87 +191,6 @@ public class AccountPlansResource extends AccountOwnedResource<AccountPlan, Acco
paymentMethod.setAccount(caller.getUuid()).validate(errors, configuration);
}
}

// apply promo code (and default) promotions
Promotion promo = null;
if (request.hasPromoCode()) {
promo = promotionDAO.findEnabledWithCode(request.getPromoCode());
if (promo == null) {
errors.addViolation("err.promoCode.notFound");
promo = null;
} else if (promo.inactive()) {
errors.addViolation("err.promoCode.notActive");
promo = null;
}
} else {
for (Promotion p : promotionDAO.findEnabledAndActiveWithNoCode()) {
if (p.active()) { // todo: add JS condition?
promo = p;
break;
}
}
}
if (promo != null) {
final CloudService promoCloud = cloudDAO.findByUuid(promo.getCloud());
if (promoCloud == null || promoCloud.getType() != CloudServiceType.payment) {
errors.addViolation("err.promoCode.configurationError");
} else {
final PaymentServiceDriver promoDriver = promoCloud.getPaymentDriver(configuration);
if (promoDriver.getPaymentMethodType() != PaymentMethodType.promotional_credit
|| !(promoDriver instanceof PromotionalPaymentServiceDriver)) {
errors.addViolation("err.promoCode.configurationError");
} else {
final PromotionalPaymentServiceDriver promoPaymentDriver = (PromotionalPaymentServiceDriver) promoDriver;
if (!promoPaymentDriver.applyPromo(promo, caller)) {
if (request.hasPromoCode()) {
errors.addViolation("err.promoCode.notApplied");
} else {
log.warn("setReferences: promo not applied: "+promo.getName());
}
}
}
}
}

// apply referral promotions
if (request.hasReferralFrom()) {
final Account referredFrom = accountDAO.findByName(request.getReferralFrom());
if (referredFrom == null || referredFrom.deleted()) {
errors.addViolation("err.referralFrom.invalid");
}
// check for referral promotion
final List<Promotion> referralPromos = promotionDAO.findEnabledAndActiveWithReferral();
if (empty(referralPromos)) {
errors.addViolation("err.referralFrom.unavailable");
} else {
Promotion referralPromo = null;
for (Promotion p : referralPromos) {
if (p.active()) { // todo: add JS condition?
referralPromo = p;
break;
}
}
if (referralPromo == null) {
errors.addViolation("err.referralFrom.unavailable");
} else {
final CloudService referralCloud = cloudDAO.findByUuid(referralPromo.getCloud());
if (referralCloud == null || referralCloud.getType() != CloudServiceType.payment) {
errors.addViolation("err.referralFrom.configurationError");
} else {
final PaymentServiceDriver referralDriver = referralCloud.getPaymentDriver(configuration);
if (referralDriver.getPaymentMethodType() != PaymentMethodType.promotional_credit
|| !(referralDriver instanceof PromotionalPaymentServiceDriver)) {
errors.addViolation("err.referralFrom.configurationError");
} else {
final PromotionalPaymentServiceDriver referralPaymentDriver = (PromotionalPaymentServiceDriver) referralDriver;
if (!referralPaymentDriver.applyReferralPromo(referralPromo, caller, referredFrom)) {
errors.addViolation("err.referralFrom.notApplied");
}
}
}
}
}
}
}
if (errors.isInvalid()) throw invalidEx(errors);



+ 135
- 0
bubble-server/src/main/java/bubble/service/bill/PromotionService.java Прегледај датотеку

@@ -0,0 +1,135 @@
package bubble.service.bill;

import bubble.cloud.CloudServiceType;
import bubble.cloud.payment.PaymentServiceDriver;
import bubble.cloud.payment.PromotionalPaymentServiceDriver;
import bubble.dao.account.AccountDAO;
import bubble.dao.account.ReferralCodeDAO;
import bubble.dao.bill.PromotionDAO;
import bubble.dao.cloud.CloudServiceDAO;
import bubble.model.account.Account;
import bubble.model.account.ReferralCode;
import bubble.model.bill.PaymentMethodType;
import bubble.model.bill.Promotion;
import bubble.model.cloud.CloudService;
import bubble.server.BubbleConfiguration;
import lombok.extern.slf4j.Slf4j;
import org.cobbzilla.wizard.validation.ValidationResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Set;
import java.util.TreeSet;

import static org.cobbzilla.util.daemon.ZillaRuntime.empty;
import static org.cobbzilla.util.daemon.ZillaRuntime.shortError;
import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx;

@Service @Slf4j
public class PromotionService {

@Autowired private PromotionDAO promotionDAO;
@Autowired private ReferralCodeDAO referralCodeDAO;
@Autowired private CloudServiceDAO cloudDAO;
@Autowired private AccountDAO accountDAO;
@Autowired private BubbleConfiguration configuration;

public void applyPromotions(Account account, String code) {
// apply promo code (or default) promotion
final Set<Promotion> promos = new TreeSet<>();
ReferralCode referralCode = null;
if (!empty(code)) {
Promotion promo = promotionDAO.findEnabledAndActiveWithCode(code);
if (promo == null) {
// check referral codes
// it might be a referral code
referralCode = referralCodeDAO.findByName(code);
if (referralCode != null && !referralCode.used()) {
// is there a referral promotion we can use?
for (Promotion p : promotionDAO.findEnabledAndActiveWithReferral()) {
promos.add(p);
break;
}
}
} else {
promos.add(promo);
}
if (promos.isEmpty()) throw invalidEx("err.promoCode.notFound");
}

// everyone gets the highest-priority default promotion, if there are any enabled and active
for (Promotion p : promotionDAO.findEnabledAndActiveWithNoCode()) {
promos.add(p);
break;
}

if (promos.isEmpty()) return; // nothing to do

for (Promotion p : promos) {
final CloudService promoCloud = cloudDAO.findByUuid(p.getCloud());
if (promoCloud == null || promoCloud.getType() != CloudServiceType.payment) {
throw invalidEx("err.promoCode.configurationError");
} else {
final PaymentServiceDriver promoDriver = promoCloud.getPaymentDriver(configuration);
if (promoDriver.getPaymentMethodType() != PaymentMethodType.promotional_credit
|| !(promoDriver instanceof PromotionalPaymentServiceDriver)) {
throw invalidEx("err.promoCode.configurationError");
} else {
final PromotionalPaymentServiceDriver promoPaymentDriver = (PromotionalPaymentServiceDriver) promoDriver;
if (p.referral()) {
if (referralCode == null) throw invalidEx("err.promoCode.notFound");
final Account referer = accountDAO.findById(referralCode.getAccountUuid());
if (referer == null || referer.deleted()) throw invalidEx("err.promoCode.notFound");
if (!promoPaymentDriver.applyReferralPromo(p, account, referer)) {
throw invalidEx("err.promoCode.notApplied");
}
} else {
if (!promoPaymentDriver.applyPromo(p, account)) {
if (!empty(code)) {
throw invalidEx("err.promoCode.notApplied");
} else {
log.warn("setReferences: promo not applied: " + p.getName());
}
}
}
}
}
}
}

public ValidationResult validatePromotions(String code) {
if (!empty(code)) {
Promotion promo = promotionDAO.findEnabledAndActiveWithCode(code);
if (promo == null) {
// it might be a referral code
final ReferralCode referralCode = referralCodeDAO.findByName(code);
if (referralCode != null && !referralCode.used()) {
final Account referer = accountDAO.findById(referralCode.getAccountUuid());
if (referer == null || referer.deleted()) return new ValidationResult("err.promoCode.notFound");

// is there a referral promotion we can use?
for (Promotion p : promotionDAO.findEnabledAndActiveWithReferral()) {
// todo: add JS check?
promo = p;
break;
}
}
if (promo == null) return new ValidationResult("err.promoCode.notFound");
}

final CloudService promoCloud = cloudDAO.findByUuid(promo.getCloud());
if (promoCloud == null || promoCloud.getType() != CloudServiceType.payment) {
return new ValidationResult("err.promoCode.configurationError");
}
// sanity check the driver
try {
final PaymentServiceDriver driver = promoCloud.getPaymentDriver(configuration);
final PromotionalPaymentServiceDriver promoDriver = (PromotionalPaymentServiceDriver) driver;
} catch (Exception e) {
log.error("validatePromotions: error applying referral promo: "+shortError(e));
return new ValidationResult("err.promoCode.configurationError");
}
}
return null;
}
}

+ 9
- 10
bubble-server/src/main/java/bubble/service/dbfilter/FilteredEntityIterator.java Прегледај датотеку

@@ -7,18 +7,19 @@ import bubble.dao.cloud.BubbleNodeKeyDAO;
import bubble.dao.device.DeviceDAO;
import bubble.model.account.Account;
import bubble.model.account.HasAccount;
import bubble.model.account.ReferralCode;
import bubble.model.account.message.AccountMessage;
import bubble.model.bill.BubblePlanApp;
import bubble.model.bill.*;
import bubble.model.cloud.BubbleNetwork;
import bubble.model.cloud.BubbleNode;
import bubble.model.cloud.BubbleNodeKey;
import bubble.model.device.Device;
import bubble.server.BubbleConfiguration;
import edu.emory.mathcs.backport.java.util.Arrays;
import lombok.extern.slf4j.Slf4j;
import org.cobbzilla.wizard.dao.DAO;
import org.cobbzilla.wizard.model.Identifiable;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;

@@ -27,14 +28,12 @@ import static org.cobbzilla.util.daemon.ZillaRuntime.die;
@Slf4j
public class FilteredEntityIterator extends EntityIterator {

public static final List<Class<? extends Identifiable>> POST_COPY_ENTITIES = new ArrayList<>();
static {
POST_COPY_ENTITIES.add(BubbleNode.class);
POST_COPY_ENTITIES.add(BubbleNodeKey.class);
POST_COPY_ENTITIES.add(Device.class);
POST_COPY_ENTITIES.add(AccountMessage.class);
}
public static boolean isPostCopyEntity(Class<? extends Identifiable> clazz) {
private static final List<Class<? extends Identifiable>> POST_COPY_ENTITIES = Arrays.asList(new Class<?>[] {
BubbleNode.class, BubbleNodeKey.class, Device.class, AccountMessage.class,
ReferralCode.class, AccountPaymentMethod.class, AccountPayment.class, Bill.class, Promotion.class,
});

private static boolean isPostCopyEntity(Class<? extends Identifiable> clazz) {
return POST_COPY_ENTITIES.stream().anyMatch(c -> c.isAssignableFrom(clazz));
}



+ 1
- 0
bubble-server/src/main/resources/message_templates/en_US/server/pre_auth/ResourceMessages.properties Прегледај датотеку

@@ -165,6 +165,7 @@ err.user.noAdmin=No admin account exists, cannot create another account
err.user.noSoleNode=Cannot create account, self-node was never initialized and multiple nodes exist
err.user.setSelfNodeFailed=Cannot create account, initialization of self-node failed
err.uuid.invalid=UUID is invalid
err.referralCode.invalid=Referral code is not valid
err.registration.disabled=Account registration is not enabled on this Bubble
err.register.alreadyLoggedIn=Cannot register a new account when logged in
err.name.registered=Username is already registered


+ 4
- 1
bubble-server/src/test/java/bubble/test/BubbleApiRunnerListener.java Прегледај датотеку

@@ -13,6 +13,7 @@ import bubble.server.BubbleConfiguration;
import bubble.service.bill.BillingService;
import com.github.jknack.handlebars.Handlebars;
import com.stripe.model.Token;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.cobbzilla.wizard.client.script.SimpleApiRunnerListener;

@@ -46,13 +47,15 @@ public class BubbleApiRunnerListener extends SimpleApiRunnerListener {
this.configuration = configuration;
}

@Getter(lazy=true) private final StripePaymentDriver stripePaymentDriver = configuration.autowire(new StripePaymentDriver());

@Override public void beforeScript(String before, Map<String, Object> ctx) throws Exception {
if (before == null) return;
if (before.startsWith(FAST_FORWARD_AND_BILL)) {
final List<String> parts = splitAndTrim(before.substring(FAST_FORWARD_AND_BILL.length()), " ");
final long delta = parseDuration(parts.get(0));
final long sleepTime = parts.size() > 1 ? parseDuration(parts.get(1)) : DEFAULT_BILLING_SLEEP;
configuration.autowire(new StripePaymentDriver()).flushCaches();
getStripePaymentDriver().flushCaches();
incrementSystemTimeOffset(delta);
configuration.getBean(BillingService.class).processBilling();
sleep(sleepTime, "waiting for BillingService to complete");


+ 1
- 3
bubble-server/src/test/java/bubble/test/FirstMonthFreePromotionTest.java Прегледај датотеку

@@ -8,8 +8,6 @@ public class FirstMonthFreePromotionTest extends PaymentTestBase {

@Override protected String getManifest() { return "manifest-1mo-promo"; }

@Test public void testFirstMonthFree () throws Exception {
modelTest("promo/first_month_free");
}
@Test public void testFirstMonthFree () throws Exception { modelTest("promo/first_month_free"); }

}

+ 3
- 3
bubble-server/src/test/java/bubble/test/ReferralMonthFreePromotionTest.java Прегледај датотеку

@@ -6,8 +6,8 @@ import org.junit.Test;
@Slf4j
public class ReferralMonthFreePromotionTest extends PaymentTestBase {

@Test public void testReferralMonthFree () throws Exception {
modelTest("promo/referral_month_free");
}
@Override protected String getManifest() { return "manifest-referral-promo"; }
@Test public void testReferralMonthFree () throws Exception { modelTest("promo/referral_month_free"); }

}

+ 5
- 0
bubble-server/src/test/resources/models/manifest-referral-promo.json Прегледај датотеку

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

+ 10
- 0
bubble-server/src/test/resources/models/system/cloudService_referral_free.json Прегледај датотеку

@@ -0,0 +1,10 @@
[
{
"name": "ReferralMonthFree",
"type": "payment",
"driverClass": "bubble.cloud.payment.referralMonthFree.ReferralMonthFreePaymentDriver",
"driverConfig": {},
"credentials": {},
"template": true
}
]

+ 10
- 0
bubble-server/src/test/resources/models/system/promotion_referral_free.json Прегледај датотеку

@@ -0,0 +1,10 @@
[
{
"name": "ReferralMonthFree",
"cloud": "ReferralMonthFree",
"priority": 1,
"currency": "USD",
"maxValue": 1200,
"referral": true
}
]

+ 5
- 17
bubble-server/src/test/resources/models/tests/promo/first_month_free.json Прегледај датотеку

@@ -1,27 +1,14 @@
[
{
"comment": "create a user account",
"comment": "register a user account",
"request": {
"uri": "users",
"method": "put",
"session": "new",
"uri": "auth/register",
"entity": {
"name": "test_user_1mo_free",
"password": "password1!",
"contact": {"type": "email", "info": "test_user_1mo_free@example.com"}
}
}
},

{
"before": "sleep 22s", // wait for account objects to be created
"comment": "login as new user",
"request": {
"session": "new",
"uri": "auth/login",
"entity": {
"name": "test_user_1mo_free",
"password": "password1!"
}
},
"response": {
"store": "testAccount",
@@ -31,6 +18,7 @@
},

{
"before": "sleep 10s",
"comment": "as root, check email inbox for verification message",
"request": {
"session": "rootSession",
@@ -377,7 +365,7 @@
},

{
"before": "fast_forward_and_bill 31d 20s",
"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": {


+ 176
- 1
bubble-server/src/test/resources/models/tests/promo/referral_month_free.json Прегледај датотеку

@@ -1 +1,176 @@
[]
[
{
"comment": "create a user account for the referring user",
"request": {
"uri": "users",
"method": "put",
"entity": {
"name": "test_user_referring_free",
"password": "password1!",
"contact": {"type": "email", "info": "test_user_referring_free@example.com"}
}
}
},

{
"before": "sleep 22s", // wait for account objects to be created
"comment": "login as referring user",
"request": {
"session": "new",
"uri": "auth/login",
"entity": {
"name": "test_user_referring_free",
"password": "password1!"
}
},
"response": {
"store": "referringUser",
"sessionName": "referringUserSession",
"session": "token"
}
},

{
"comment": "as root, check email inbox for verification message for referring user",
"request": {
"session": "rootSession",
"uri": "debug/inbox/email/test_user_referring_free@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": "as root, grant some referral codes to the referring user",
"request": {
"uri": "users/test_user_referring_free/referralCodes",
"method": "put",
"entity": { "count": 3 }
},
"response": {
"store": "referralCodes",
"check": [ {"condition": "json.length === 3"} ]
}
},

{
"comment": "approve email verification request for referring user",
"request": {
"session": "referringUserSession",
"uri": "auth/approve/{{emailInbox.[0].ctx.confirmationToken}}",
"method": "post"
}
},

{
"comment": "as referring user, list referral codes, verify all codes are unused",
"request": { "uri": "me/referralCodes" },
"response": {
"check": [
{"condition": "json.length === 3"},
{"condition": "json[0].getUsedBy() === null"},
{"condition": "json[1].getUsedBy() === null"},
{"condition": "json[2].getUsedBy() === null"}
]
}
},

{
"comment": "register an account for the referred user, using one of the referral codes",
"request": {
"session": "new",
"uri": "auth/register",
"entity": {
"name": "test_user_referred_free",
"password": "password1!",
"contact": {"type": "email", "info": "test_user_referred_free@example.com"},
"promoCode": "{{referralCodes.[0].name}}"
}
},
"response": {
"store": "referredUser",
"sessionName": "referredUserSession",
"session": "token"
}
},

{
"before": "sleep 22s", // wait for account objects to be created
"comment": "as root, check email inbox for verification message",
"request": {
"session": "rootSession",
"uri": "debug/inbox/email/test_user_referred_free@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": "as referred user, approve email verification request",
"request": {
"session": "referredUserSession",
"uri": "auth/approve/{{emailInbox.[0].ctx.confirmationToken}}",
"method": "post"
}
},

{
"comment": "as referred user, lookup payment methods, ensure ReferralMonthFree is present",
"request": { "uri": "me/paymentMethods" },
"response": {
"check": [
{"condition": "json.length === 1"},
{"condition": "json[0].getPaymentMethodType().name() === 'promotional_credit'"},
{"condition": "json[0].deleted() === false"}
]
}
},

{
"comment": "as referring user, lookup payment methods, ensure ReferralMonthFree is present",
"request": {
"session": "referringUserSession",
"uri": "me/paymentMethods"
},
"response": {
"check": [
{"condition": "json.length === 1"},
{"condition": "json[0].getPaymentMethodType().name() === 'promotional_credit'"},
{"condition": "json[0].deleted() === false"}
]
}
},

{
"comment": "get plans",
"request": { "uri": "plans" },
"response": {
"store": "plans",
"check": [{"condition": "json.length >= 1"}]
}
},

{
"comment": "get payment methods, tokenize a credit card",
"request": { "uri": "paymentMethods" },
"response": {
"store": "paymentMethods"
},
"after": "stripe_tokenize_card"
}

// start a network. we don't get the first month free because the user on the other end of
// the ReferralMonthFree has not made any payments
]

Loading…
Откажи
Сачувај