@@ -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"; | |||
@@ -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; | |||
} | |||
@@ -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; | |||
} | |||
} |
@@ -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); | |||
@@ -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); } | |||
} |
@@ -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()); | |||
} | |||
} |
@@ -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; } | |||
@@ -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; } | |||
@@ -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; | |||
} |
@@ -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 | |||
@@ -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; | |||
@@ -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) | |||
@@ -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) | |||
@@ -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); | |||
} | |||
} |
@@ -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); | |||
@@ -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; | |||
} | |||
} |
@@ -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)); | |||
} | |||
@@ -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 | |||
@@ -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"); | |||
@@ -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"); } | |||
} |
@@ -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"); } | |||
} |
@@ -0,0 +1,5 @@ | |||
[ | |||
"manifest-test", | |||
"system/cloudService_referral_free", | |||
"system/promotion_referral_free" | |||
] |
@@ -0,0 +1,10 @@ | |||
[ | |||
{ | |||
"name": "ReferralMonthFree", | |||
"type": "payment", | |||
"driverClass": "bubble.cloud.payment.referralMonthFree.ReferralMonthFreePaymentDriver", | |||
"driverConfig": {}, | |||
"credentials": {}, | |||
"template": true | |||
} | |||
] |
@@ -0,0 +1,10 @@ | |||
[ | |||
{ | |||
"name": "ReferralMonthFree", | |||
"cloud": "ReferralMonthFree", | |||
"priority": 1, | |||
"currency": "USD", | |||
"maxValue": 1200, | |||
"referral": true | |||
} | |||
] |
@@ -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": { | |||
@@ -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 | |||
] |