@@ -7,6 +7,8 @@ import bubble.model.bill.*; | |||||
import lombok.extern.slf4j.Slf4j; | import lombok.extern.slf4j.Slf4j; | ||||
import org.springframework.beans.factory.annotation.Autowired; | import org.springframework.beans.factory.annotation.Autowired; | ||||
import java.util.List; | |||||
import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx; | import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx; | ||||
@Slf4j | @Slf4j | ||||
@@ -60,7 +62,7 @@ public abstract class PaymentDriverBase<T> extends CloudServiceDriverBase<T> imp | |||||
return bill; | return bill; | ||||
} | } | ||||
@Override public boolean authorize(BubblePlan plan, AccountPaymentMethod paymentMethod) { | |||||
@Override public boolean authorize(BubblePlan plan, String accountPlanUuid, AccountPaymentMethod paymentMethod) { | |||||
return true; | return true; | ||||
} | } | ||||
@@ -131,7 +133,14 @@ public abstract class PaymentDriverBase<T> extends CloudServiceDriverBase<T> imp | |||||
// mark the bill as paid, enable the plan | // mark the bill as paid, enable the plan | ||||
billDAO.update(bill.setPayment(accountPayment.getUuid()).setStatus(BillStatus.paid)); | billDAO.update(bill.setPayment(accountPayment.getUuid()).setStatus(BillStatus.paid)); | ||||
accountPlanDAO.update(accountPlan.setEnabled(true)); | |||||
// if there are no unpaid bills, we can (re-)enable the plan | |||||
final List<Bill> unpaidBills = billDAO.findUnpaidByAccountPlan(accountPlan.getUuid()); | |||||
if (unpaidBills.isEmpty()) { | |||||
accountPlanDAO.update(accountPlan.setEnabled(true)); | |||||
} else { | |||||
accountPlanDAO.update(accountPlan.setEnabled(false)); | |||||
} | |||||
return accountPayment; | return accountPayment; | ||||
} | } | ||||
@@ -181,7 +190,11 @@ public abstract class PaymentDriverBase<T> extends CloudServiceDriverBase<T> imp | |||||
} | } | ||||
// Determine how much to refund | // Determine how much to refund | ||||
final long refundAmount = plan.getPeriod().calculateRefund(accountPlan.getCtime(), bill.getPeriod(), bill.getTotal()); | |||||
final long refundAmount = plan.getPeriod().calculateRefund(bill, accountPlan); | |||||
if (refundAmount == 0) { | |||||
log.warn("refund: no refund to issue, refundAmount == 0"); | |||||
return true; | |||||
} | |||||
final String refundInfo; | final String refundInfo; | ||||
try { | try { | ||||
@@ -17,7 +17,7 @@ public interface PaymentServiceDriver { | |||||
default PaymentValidationResult claim(AccountPaymentMethod paymentMethod) { return notSupported("claim"); } | default PaymentValidationResult claim(AccountPaymentMethod paymentMethod) { return notSupported("claim"); } | ||||
default PaymentValidationResult claim(AccountPlan accountPlan) { return notSupported("claim"); } | default PaymentValidationResult claim(AccountPlan accountPlan) { return notSupported("claim"); } | ||||
boolean authorize(BubblePlan plan, AccountPaymentMethod paymentMethod); | |||||
boolean authorize(BubblePlan plan, String accountPlanUuid, AccountPaymentMethod paymentMethod); | |||||
boolean purchase(String accountPlanUuid, String paymentMethodUuid, String billUuid); | boolean purchase(String accountPlanUuid, String paymentMethodUuid, String billUuid); | ||||
@@ -52,12 +52,13 @@ public class DelegatedPaymentDriver extends DelegatedCloudServiceDriverBase impl | |||||
new PaymentMethodClaimNotification(cloud.getName(), accountPlan)); | new PaymentMethodClaimNotification(cloud.getName(), accountPlan)); | ||||
} | } | ||||
@Override public boolean authorize(BubblePlan plan, AccountPaymentMethod paymentMethod) { | |||||
@Override public boolean authorize(BubblePlan plan, String accountPlanUuid, AccountPaymentMethod paymentMethod) { | |||||
final BubbleNode delegate = getDelegateNode(); | final BubbleNode delegate = getDelegateNode(); | ||||
final PaymentResult result = notificationService.notifySync(delegate, payment_driver_authorize, | final PaymentResult result = notificationService.notifySync(delegate, payment_driver_authorize, | ||||
new PaymentNotification() | new PaymentNotification() | ||||
.setCloud(cloud.getName()) | .setCloud(cloud.getName()) | ||||
.setPlanUuid(plan.getUuid()) | .setPlanUuid(plan.getUuid()) | ||||
.setAccountPlanUuid(accountPlanUuid) | |||||
.setPaymentMethodUuid(paymentMethod.getUuid())); | .setPaymentMethodUuid(paymentMethod.getUuid())); | ||||
return processResult(result); | return processResult(result); | ||||
} | } | ||||
@@ -89,7 +90,7 @@ public class DelegatedPaymentDriver extends DelegatedCloudServiceDriverBase impl | |||||
if (result.hasViolations()) { | if (result.hasViolations()) { | ||||
throw invalidEx(result.violationList()); | throw invalidEx(result.violationList()); | ||||
} | } | ||||
if (result.hasError()) return die("authorize: "+result.getError()); | |||||
if (result.hasError()) return die("processResult: "+result.getError()); | |||||
return false; | return false; | ||||
} | } | ||||
@@ -126,6 +126,7 @@ public class StripePaymentDriver extends PaymentDriverBase<StripePaymentDriverCo | |||||
} | } | ||||
@Override public boolean authorize(BubblePlan plan, | @Override public boolean authorize(BubblePlan plan, | ||||
String accountPlanUuid, | |||||
AccountPaymentMethod paymentMethod) { | AccountPaymentMethod paymentMethod) { | ||||
final String planUuid = plan.getUuid(); | final String planUuid = plan.getUuid(); | ||||
final String paymentMethodUuid = paymentMethod.getUuid(); | final String paymentMethodUuid = paymentMethod.getUuid(); | ||||
@@ -141,7 +142,7 @@ public class StripePaymentDriver extends PaymentDriverBase<StripePaymentDriverCo | |||||
chargeParams.put("statement_descriptor", plan.chargeDescription()); | chargeParams.put("statement_descriptor", plan.chargeDescription()); | ||||
chargeParams.put("capture", false); | chargeParams.put("capture", false); | ||||
final String chargeJson = json(chargeParams, COMPACT_MAPPER); | final String chargeJson = json(chargeParams, COMPACT_MAPPER); | ||||
final String authCacheKey = getAuthCacheKey(planUuid, paymentMethodUuid); | |||||
final String authCacheKey = getAuthCacheKey(accountPlanUuid, paymentMethodUuid); | |||||
final String chargeId = authCache.get(authCacheKey); | final String chargeId = authCache.get(authCacheKey); | ||||
if (chargeId != null) { | if (chargeId != null) { | ||||
log.warn("authorize: already authorized: "+authCacheKey); | log.warn("authorize: already authorized: "+authCacheKey); | ||||
@@ -208,7 +209,7 @@ public class StripePaymentDriver extends PaymentDriverBase<StripePaymentDriverCo | |||||
final RedisService authCache = getAuthCache(); | final RedisService authCache = getAuthCache(); | ||||
final RedisService chargeCache = getChargeCache(); | final RedisService chargeCache = getChargeCache(); | ||||
final String authCacheKey = getAuthCacheKey(plan.getUuid(), paymentMethodUuid); | |||||
final String authCacheKey = getAuthCacheKey(accountPlanUuid, paymentMethodUuid); | |||||
try { | try { | ||||
final String charged = chargeCache.get(billUuid); | final String charged = chargeCache.get(billUuid); | ||||
if (charged != null) { | if (charged != null) { | ||||
@@ -18,8 +18,10 @@ import java.util.List; | |||||
import static java.util.concurrent.TimeUnit.SECONDS; | import static java.util.concurrent.TimeUnit.SECONDS; | ||||
import static org.cobbzilla.util.daemon.ZillaRuntime.background; | import static org.cobbzilla.util.daemon.ZillaRuntime.background; | ||||
import static org.cobbzilla.util.daemon.ZillaRuntime.now; | |||||
import static org.cobbzilla.util.system.Sleep.sleep; | import static org.cobbzilla.util.system.Sleep.sleep; | ||||
import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx; | import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx; | ||||
import static org.hibernate.criterion.Restrictions.*; | |||||
@Repository | @Repository | ||||
public class AccountPlanDAO extends AccountOwnedEntityDAO<AccountPlan> { | public class AccountPlanDAO extends AccountOwnedEntityDAO<AccountPlan> { | ||||
@@ -36,11 +38,18 @@ public class AccountPlanDAO extends AccountOwnedEntityDAO<AccountPlan> { | |||||
} | } | ||||
public List<AccountPlan> findByAccountAndNotDeleted(String account) { | public List<AccountPlan> findByAccountAndNotDeleted(String account) { | ||||
return findByFields("account", account, "deleted", false); | |||||
return findByFields("account", account, "deleted", null); | |||||
} | } | ||||
public List<AccountPlan> findByAccountAndPaymentMethodAndNotDeleted(String account, String paymentMethod) { | public List<AccountPlan> findByAccountAndPaymentMethodAndNotDeleted(String account, String paymentMethod) { | ||||
return findByFields("account", account, "paymentMethod", paymentMethod, "deleted", false); | |||||
return findByFields("account", account, "paymentMethod", paymentMethod, "deleted", null); | |||||
} | |||||
public List<AccountPlan> findByDeletedAndNotClosed() { | |||||
return list(criteria().add(and( | |||||
isNotNull("deleted"), | |||||
eq("closed", false) | |||||
))); | |||||
} | } | ||||
@Override public Object preCreate(AccountPlan accountPlan) { | @Override public Object preCreate(AccountPlan accountPlan) { | ||||
@@ -62,7 +71,8 @@ public class AccountPlanDAO extends AccountOwnedEntityDAO<AccountPlan> { | |||||
} | } | ||||
if (paymentDriver.getPaymentMethodType().requiresAuth()) { | if (paymentDriver.getPaymentMethodType().requiresAuth()) { | ||||
final BubblePlan plan = planDAO.findByUuid(accountPlan.getPlan()); | final BubblePlan plan = planDAO.findByUuid(accountPlan.getPlan()); | ||||
paymentDriver.authorize(plan, accountPlan.getPaymentMethodObject()); | |||||
accountPlan.beforeCreate(); // ensure uuid exists | |||||
paymentDriver.authorize(plan, accountPlan.getUuid(), accountPlan.getPaymentMethodObject()); | |||||
} | } | ||||
accountPlan.setPaymentMethod(accountPlan.getPaymentMethodObject().getUuid()); | accountPlan.setPaymentMethod(accountPlan.getPaymentMethodObject().getUuid()); | ||||
} | } | ||||
@@ -81,6 +91,8 @@ public class AccountPlanDAO extends AccountOwnedEntityDAO<AccountPlan> { | |||||
.setPrice(plan.getPrice()) | .setPrice(plan.getPrice()) | ||||
.setCurrency(plan.getCurrency()) | .setCurrency(plan.getCurrency()) | ||||
.setPeriod(plan.getPeriod().currentPeriod()) | .setPeriod(plan.getPeriod().currentPeriod()) | ||||
.setPeriodStart(plan.getPeriod().getFirstPeriodStart()) | |||||
.setPeriodEnd(plan.getPeriod().getFirstPeriodEnd()) | |||||
.setQuantity(1L) | .setQuantity(1L) | ||||
.setType(BillItemType.compute) | .setType(BillItemType.compute) | ||||
.setStatus(BillStatus.unpaid)); | .setStatus(BillStatus.unpaid)); | ||||
@@ -106,7 +118,7 @@ public class AccountPlanDAO extends AccountOwnedEntityDAO<AccountPlan> { | |||||
if (network != null && network.getState() != BubbleNetworkState.stopped) { | if (network != null && network.getState() != BubbleNetworkState.stopped) { | ||||
throw invalidEx("err.accountPlan.stopNetworkBeforeDeleting"); | throw invalidEx("err.accountPlan.stopNetworkBeforeDeleting"); | ||||
} | } | ||||
update(accountPlan.setDeleted(true).setEnabled(false)); | |||||
update(accountPlan.setDeleted(now()).setEnabled(false)); | |||||
if (configuration.paymentsEnabled()) { | if (configuration.paymentsEnabled()) { | ||||
refundService.processRefunds(); | refundService.processRefunds(); | ||||
} | } | ||||
@@ -2,6 +2,7 @@ package bubble.dao.bill; | |||||
import bubble.dao.account.AccountOwnedEntityDAO; | import bubble.dao.account.AccountOwnedEntityDAO; | ||||
import bubble.model.bill.Bill; | import bubble.model.bill.Bill; | ||||
import bubble.model.bill.BillStatus; | |||||
import org.hibernate.criterion.Order; | import org.hibernate.criterion.Order; | ||||
import org.springframework.stereotype.Repository; | import org.springframework.stereotype.Repository; | ||||
@@ -22,4 +23,8 @@ public class BillDAO extends AccountOwnedEntityDAO<Bill> { | |||||
return bills.isEmpty() ? null : bills.get(0); | return bills.isEmpty() ? null : bills.get(0); | ||||
} | } | ||||
public List<Bill> findUnpaidByAccountPlan(String accountPlanUuid) { | |||||
return findByFields("accountPlan", accountPlanUuid, "status", BillStatus.unpaid); | |||||
} | |||||
} | } |
@@ -38,6 +38,8 @@ public class AccountPlan extends IdentifiableBase implements HasAccount { | |||||
@SuppressWarnings("unused") | @SuppressWarnings("unused") | ||||
public AccountPlan (AccountPlan other) { copy(this, other, CREATE_FIELDS); } | public AccountPlan (AccountPlan other) { copy(this, other, CREATE_FIELDS); } | ||||
@Override public void beforeCreate() { if (!hasUuid()) initUuid(); } | |||||
// mirrors network name | // mirrors network name | ||||
@Size(max=100, message="err.name.length") | @Size(max=100, message="err.name.length") | ||||
@Column(length=100, nullable=false) | @Column(length=100, nullable=false) | ||||
@@ -67,13 +69,12 @@ public class AccountPlan extends IdentifiableBase implements HasAccount { | |||||
@Getter @Setter private Boolean enabled = false; | @Getter @Setter private Boolean enabled = false; | ||||
public boolean enabled() { return enabled != null && enabled; } | public boolean enabled() { return enabled != null && enabled; } | ||||
@Column(nullable=false) | |||||
@Getter @Setter private Boolean deleted = false; | |||||
public boolean deleted() { return deleted != null && deleted; } | |||||
@ECIndex @Getter @Setter private Long deleted; | |||||
public boolean deleted() { return deleted != null; } | |||||
public boolean notDeleted() { return !deleted(); } | public boolean notDeleted() { return !deleted(); } | ||||
@Column(nullable=false) | @Column(nullable=false) | ||||
@Getter @Setter private Boolean closed = false; | |||||
@ECIndex @Getter @Setter private Boolean closed = false; | |||||
public boolean closed() { return closed != null && closed; } | public boolean closed() { return closed != null && closed; } | ||||
public boolean notClosed() { return !closed(); } | public boolean notClosed() { return !closed(); } | ||||
@@ -51,9 +51,17 @@ public class Bill extends IdentifiableBase implements HasAccountNoName { | |||||
@Column(nullable=false, updatable=false, length=20) | @Column(nullable=false, updatable=false, length=20) | ||||
@Getter @Setter private BillItemType type; | @Getter @Setter private BillItemType type; | ||||
@Column(nullable=false, updatable=false, length=50) | |||||
@Column(nullable=false, updatable=false, length=20) | |||||
@ECIndex @Getter @Setter private String period; | @ECIndex @Getter @Setter private String period; | ||||
@Column(nullable=false, updatable=false, length=20) | |||||
@Getter @Setter private String periodStart; | |||||
@Column(nullable=false, updatable=false, length=20) | |||||
@Getter @Setter private String periodEnd; | |||||
public int daysInPeriod () { return BillPeriod.daysInPeriod(periodStart, periodEnd); } | |||||
@Type(type=ENCRYPTED_LONG) @Column(updatable=false, columnDefinition="varchar("+(ENC_LONG)+") NOT NULL") | @Type(type=ENCRYPTED_LONG) @Column(updatable=false, columnDefinition="varchar("+(ENC_LONG)+") NOT NULL") | ||||
@Getter @Setter private Long quantity = 0L; | @Getter @Setter private Long quantity = 0L; | ||||
@@ -5,6 +5,9 @@ import com.fasterxml.jackson.annotation.JsonCreator; | |||||
import lombok.AllArgsConstructor; | import lombok.AllArgsConstructor; | ||||
import lombok.Getter; | import lombok.Getter; | ||||
import org.joda.time.DateTime; | import org.joda.time.DateTime; | ||||
import org.joda.time.Days; | |||||
import org.joda.time.DurationFieldType; | |||||
import org.joda.time.format.DateTimeFormat; | |||||
import org.joda.time.format.DateTimeFormatter; | import org.joda.time.format.DateTimeFormatter; | ||||
import static bubble.ApiConstants.enumFromString; | import static bubble.ApiConstants.enumFromString; | ||||
@@ -17,17 +20,30 @@ public enum BillPeriod { | |||||
monthly (DATE_FORMAT_YYYY_MM); | monthly (DATE_FORMAT_YYYY_MM); | ||||
public static final DateTimeFormatter BILL_START_END_FORMAT = DateTimeFormat.forPattern("yyyy-MM-dd"); | |||||
@Getter private DateTimeFormatter formatter; | @Getter private DateTimeFormatter formatter; | ||||
@Getter(lazy=true) private final BillPeriodDriver driver = instantiate(getClass().getName()+"_"+name()); | |||||
@Getter(lazy=true) private final BillPeriodDriver driver = instantiate(BillPeriodDriver.class.getName()+"_"+name()); | |||||
@JsonCreator public static BillPeriod fromString (String v) { return enumFromString(BillPeriod.class, v); } | @JsonCreator public static BillPeriod fromString (String v) { return enumFromString(BillPeriod.class, v); } | ||||
public static int daysInPeriod(String periodStart, String periodEnd) { | |||||
final DateTime start = new DateTime(BILL_START_END_FORMAT.parseMillis(periodStart)); | |||||
final DateTime end = new DateTime(BILL_START_END_FORMAT.parseMillis(periodEnd)); | |||||
return Days.daysBetween(start.withTimeAtStartOfDay(), end.withTimeAtStartOfDay()).getDays(); | |||||
} | |||||
public String currentPeriod() { return formatter.print(now()); } | public String currentPeriod() { return formatter.print(now()); } | ||||
public long calculateRefund(long planStart, String period, long total) { | |||||
return getDriver().calculateRefund(planStart, period, total); | |||||
public long calculateRefund(Bill bill, AccountPlan accountPlan) { | |||||
return getDriver().calculateRefund(bill, accountPlan); | |||||
} | } | ||||
public DateTime nextPeriod() { return getDriver().nextPeriod(); } | |||||
public String getFirstPeriodStart() { return BILL_START_END_FORMAT.print(now()); } | |||||
public String getFirstPeriodEnd() { | |||||
final DurationFieldType fieldType = getDriver().getDurationFieldType(); | |||||
return BILL_START_END_FORMAT.print(new DateTime(now()).withTimeAtStartOfDay().withFieldAdded(fieldType, 1)); | |||||
} | |||||
} | } |
@@ -1,11 +1,13 @@ | |||||
package bubble.model.bill.period; | package bubble.model.bill.period; | ||||
import org.joda.time.DateTime; | |||||
import bubble.model.bill.AccountPlan; | |||||
import bubble.model.bill.Bill; | |||||
import org.joda.time.DurationFieldType; | |||||
public interface BillPeriodDriver { | public interface BillPeriodDriver { | ||||
long calculateRefund(long planStart, String period, long total); | |||||
long calculateRefund(Bill bill, AccountPlan plan); | |||||
DateTime nextPeriod(); | |||||
DurationFieldType getDurationFieldType(); | |||||
} | } |
@@ -1,52 +1,37 @@ | |||||
package bubble.model.bill.period; | package bubble.model.bill.period; | ||||
import bubble.model.bill.BillPeriod; | |||||
import bubble.model.bill.AccountPlan; | |||||
import bubble.model.bill.Bill; | |||||
import lombok.extern.slf4j.Slf4j; | import lombok.extern.slf4j.Slf4j; | ||||
import org.joda.time.DateTime; | import org.joda.time.DateTime; | ||||
import org.joda.time.Days; | import org.joda.time.Days; | ||||
import org.joda.time.LocalDate; | |||||
import org.joda.time.DurationFieldType; | |||||
import java.math.RoundingMode; | import java.math.RoundingMode; | ||||
import static bubble.model.bill.BillPeriod.BILL_START_END_FORMAT; | |||||
import static org.cobbzilla.util.daemon.ZillaRuntime.big; | import static org.cobbzilla.util.daemon.ZillaRuntime.big; | ||||
import static org.cobbzilla.util.daemon.ZillaRuntime.now; | |||||
import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx; | |||||
@Slf4j | @Slf4j | ||||
public class BillPeriodDriver_monthly implements BillPeriodDriver { | public class BillPeriodDriver_monthly implements BillPeriodDriver { | ||||
@Override public long calculateRefund(long planStart, String period, long total) { | |||||
final DateTime currentTime = new DateTime(now()).withTimeAtStartOfDay(); | |||||
final DateTime periodStart = BillPeriod.monthly.getFormatter().parseDateTime(period); | |||||
final DateTime nextPeriodStart = nextPeriod(); | |||||
@Override public DurationFieldType getDurationFieldType() { return DurationFieldType.months(); } | |||||
if (currentTime.getMillis() < periodStart.getMillis()) { | |||||
// this should never happen | |||||
log.error("calculateRefund: currentTime + 1 day is BEFORE periodStart, invalid"); | |||||
throw invalidEx("err.refund.periodError"); | |||||
} | |||||
if (currentTime.getMillis() > nextPeriodStart.getMillis()) { | |||||
log.warn("calculateRefund: currentTime + 1 day is after nextPeriodStart, no refund"); | |||||
return 0L; | |||||
} | |||||
@Override public long calculateRefund(Bill bill, AccountPlan plan) { | |||||
final DateTime endTime = new DateTime(plan.getDeleted()).plusDays(1).withTimeAtStartOfDay(); | |||||
final DateTime startPeriod = BILL_START_END_FORMAT.parseDateTime(bill.getPeriodStart()); | |||||
final DateTime endPeriod = BILL_START_END_FORMAT.parseDateTime(bill.getPeriodEnd()); | |||||
final int daysInPeriod = Days.daysBetween(periodStart, nextPeriodStart).getDays(); | |||||
final int daysUsed = Days.daysBetween(periodStart, currentTime).getDays(); | |||||
if (daysUsed == 0) { | |||||
log.warn("calculateRefund: no days used, assuming 1 day used"); | |||||
return prorated(total, daysInPeriod - 1, daysInPeriod); | |||||
} else { | |||||
return prorated(total, daysInPeriod - daysUsed, daysInPeriod); | |||||
final int daysInPeriod = Days.daysBetween(startPeriod, endPeriod).getDays(); | |||||
final int daysRemaining = Days.daysBetween(endTime, endPeriod).getDays(); | |||||
if (daysRemaining == 0) { | |||||
log.info("calculateRefund: no days remaining in period, no refund to issue"); | |||||
return 0L; | |||||
} | } | ||||
return big(bill.getTotal()) | |||||
.multiply(big(daysRemaining)) | |||||
.divide(big(daysInPeriod), RoundingMode.HALF_EVEN) | |||||
.longValue(); | |||||
} | } | ||||
private long prorated(long total, int daysUsed, int daysInPeriod) { | |||||
return big(total) | |||||
.multiply(big(daysUsed)) | |||||
.divide(big(daysInPeriod), RoundingMode.HALF_EVEN).longValue(); | |||||
} | |||||
@Override public DateTime nextPeriod() { | |||||
return new LocalDate(now()).plusMonths(1).withDayOfMonth(1).toDateTimeAtStartOfDay(); | |||||
} | |||||
} | } |
@@ -1,7 +1,7 @@ | |||||
package bubble.notify.payment; | package bubble.notify.payment; | ||||
import bubble.cloud.payment.PaymentServiceDriver; | |||||
import bubble.dao.bill.AccountPaymentMethodDAO; | import bubble.dao.bill.AccountPaymentMethodDAO; | ||||
import bubble.dao.bill.AccountPlanDAO; | |||||
import bubble.dao.bill.BubblePlanDAO; | import bubble.dao.bill.BubblePlanDAO; | ||||
import bubble.model.bill.AccountPaymentMethod; | import bubble.model.bill.AccountPaymentMethod; | ||||
import bubble.model.bill.BubblePlan; | import bubble.model.bill.BubblePlan; | ||||
@@ -12,12 +12,12 @@ public class NotificationHandler_payment_driver_authorize extends NotificationHa | |||||
@Autowired private BubblePlanDAO planDAO; | @Autowired private BubblePlanDAO planDAO; | ||||
@Autowired private AccountPaymentMethodDAO paymentMethodDAO; | @Autowired private AccountPaymentMethodDAO paymentMethodDAO; | ||||
@Autowired private AccountPlanDAO accountPlanDAO; | |||||
@Override public boolean handlePaymentRequest(PaymentNotification paymentNotification, CloudService paymentService) { | @Override public boolean handlePaymentRequest(PaymentNotification paymentNotification, CloudService paymentService) { | ||||
final BubblePlan plan = planDAO.findByUuid(paymentNotification.getPlanUuid()); | final BubblePlan plan = planDAO.findByUuid(paymentNotification.getPlanUuid()); | ||||
final AccountPaymentMethod paymentMethod = paymentMethodDAO.findByUuid(paymentNotification.getPaymentMethodUuid()); | final AccountPaymentMethod paymentMethod = paymentMethodDAO.findByUuid(paymentNotification.getPaymentMethodUuid()); | ||||
return paymentService.getPaymentDriver(configuration).authorize(plan, paymentMethod); | |||||
final PaymentServiceDriver paymentDriver = paymentService.getPaymentDriver(configuration); | |||||
return paymentDriver.authorize(plan, paymentNotification.getAccountPlanUuid(), paymentMethod); | |||||
} | } | ||||
} | } |
@@ -47,11 +47,6 @@ public class AccountPlansResource extends AccountOwnedResource<AccountPlan, Acco | |||||
public AccountPlansResource(Account account) { super(account); } | public AccountPlansResource(Account account) { super(account); } | ||||
@Override protected AccountPlan find(ContainerRequest ctx, String id) { | |||||
final AccountPlan accountPlan = super.find(ctx, id); | |||||
return accountPlan == null || accountPlan.deleted() ? null : accountPlan; | |||||
} | |||||
@Override protected List<AccountPlan> list(ContainerRequest ctx) { | @Override protected List<AccountPlan> list(ContainerRequest ctx) { | ||||
return getDao().findByAccountAndNotDeleted(account.getUuid()); | return getDao().findByAccountAndNotDeleted(account.getUuid()); | ||||
} | } | ||||
@@ -9,6 +9,7 @@ import bubble.dao.cloud.CloudServiceDAO; | |||||
import bubble.model.cloud.BubbleNode; | import bubble.model.cloud.BubbleNode; | ||||
import bubble.model.cloud.BubbleNodeKey; | import bubble.model.cloud.BubbleNodeKey; | ||||
import bubble.server.BubbleConfiguration; | import bubble.server.BubbleConfiguration; | ||||
import bubble.service.bill.RefundService; | |||||
import bubble.service.boot.SageHelloService; | import bubble.service.boot.SageHelloService; | ||||
import lombok.extern.slf4j.Slf4j; | import lombok.extern.slf4j.Slf4j; | ||||
import org.cobbzilla.util.string.StringUtil; | import org.cobbzilla.util.string.StringUtil; | ||||
@@ -109,6 +110,12 @@ public class NodeInitializerListener extends RestServerLifecycleListenerBase<Bub | |||||
c.getBean(SageHelloService.class).start(); | c.getBean(SageHelloService.class).start(); | ||||
} | } | ||||
// start RefundService if payments are enabled and this is a SageLauncher | |||||
if (c.paymentsEnabled() && c.isSageLauncher()) { | |||||
log.info("onStart: starting RefundService"); | |||||
c.getBean(RefundService.class).start(); | |||||
} | |||||
return true; | return true; | ||||
} | } | ||||
} | } |
@@ -1,16 +1,51 @@ | |||||
package bubble.service.bill; | package bubble.service.bill; | ||||
import bubble.cloud.payment.PaymentServiceDriver; | |||||
import bubble.dao.bill.AccountPaymentMethodDAO; | import bubble.dao.bill.AccountPaymentMethodDAO; | ||||
import bubble.dao.bill.AccountPlanDAO; | |||||
import bubble.dao.cloud.CloudServiceDAO; | |||||
import bubble.model.bill.AccountPaymentMethod; | |||||
import bubble.model.bill.AccountPlan; | |||||
import bubble.model.cloud.CloudService; | |||||
import bubble.server.BubbleConfiguration; | |||||
import lombok.extern.slf4j.Slf4j; | |||||
import org.cobbzilla.util.daemon.SimpleDaemon; | |||||
import org.springframework.beans.factory.annotation.Autowired; | import org.springframework.beans.factory.annotation.Autowired; | ||||
import org.springframework.stereotype.Service; | import org.springframework.stereotype.Service; | ||||
@Service | |||||
public class RefundService { | |||||
import java.util.List; | |||||
import static java.util.concurrent.TimeUnit.HOURS; | |||||
@Service @Slf4j | |||||
public class RefundService extends SimpleDaemon { | |||||
private static final long REFUND_CHECK_INTERVAL = HOURS.toMillis(6); | |||||
@Autowired private AccountPlanDAO accountPlanDAO; | |||||
@Autowired private CloudServiceDAO cloudDAO; | |||||
@Autowired private AccountPaymentMethodDAO paymentMethodDAO; | @Autowired private AccountPaymentMethodDAO paymentMethodDAO; | ||||
@Autowired private BubbleConfiguration configuration; | |||||
public void processRefunds () { interrupt(); } | |||||
@Override protected long getSleepTime() { return REFUND_CHECK_INTERVAL; } | |||||
@Override protected boolean canInterruptSleep() { return true; } | |||||
public void processRefunds () { | |||||
// todo: wake up background job to look for AccountPlans that have been deleted but not refunded | |||||
@Override protected void process() { | |||||
// iterate over all account plans that have been deleted but not yet closed | |||||
final List<AccountPlan> pendingPlans = accountPlanDAO.findByDeletedAndNotClosed(); | |||||
for (AccountPlan accountPlan : pendingPlans) { | |||||
try { | |||||
final AccountPaymentMethod paymentMethod = paymentMethodDAO.findByUuid(accountPlan.getPaymentMethod()); | |||||
final CloudService paymentCloud = cloudDAO.findByUuid(paymentMethod.getCloud()); | |||||
final PaymentServiceDriver paymentDriver = paymentCloud.getPaymentDriver(configuration); | |||||
paymentDriver.refund(accountPlan.getUuid()); | |||||
} catch (Exception e) { | |||||
log.error("process: error processing refund for AccountPlan: "+accountPlan.getUuid()); | |||||
} | |||||
} | |||||
} | } | ||||
} | } |
@@ -30,8 +30,6 @@ import static org.cobbzilla.util.system.CommandShell.loadShellExportsOrDie; | |||||
@Slf4j | @Slf4j | ||||
public abstract class BubbleModelTestBase extends ApiModelTestBase<BubbleConfiguration, BubbleServer> { | public abstract class BubbleModelTestBase extends ApiModelTestBase<BubbleConfiguration, BubbleServer> { | ||||
public static final String[] SQL_POST_SCRIPTS = {"models/constraints.sql"}; | |||||
public static final List<RestServerLifecycleListener> TEST_LIFECYCLE_LISTENERS = asList(new RestServerLifecycleListener[] { | public static final List<RestServerLifecycleListener> TEST_LIFECYCLE_LISTENERS = asList(new RestServerLifecycleListener[] { | ||||
new NodeInitializerListener() | new NodeInitializerListener() | ||||
}); | }); | ||||
@@ -1,6 +1,7 @@ | |||||
package bubble.test; | package bubble.test; | ||||
import bubble.server.BubbleConfiguration; | import bubble.server.BubbleConfiguration; | ||||
import bubble.service.bill.RefundService; | |||||
import lombok.extern.slf4j.Slf4j; | import lombok.extern.slf4j.Slf4j; | ||||
import org.cobbzilla.wizard.server.RestServer; | import org.cobbzilla.wizard.server.RestServer; | ||||
import org.junit.Test; | import org.junit.Test; | ||||
@@ -18,6 +19,12 @@ public class PaymentTest extends ActivatedBubbleModelTestBase { | |||||
super.beforeStart(server); | super.beforeStart(server); | ||||
} | } | ||||
@Override public void onStart(RestServer<BubbleConfiguration> server) { | |||||
final BubbleConfiguration configuration = server.getConfiguration(); | |||||
configuration.getBean(RefundService.class).start(); // ensure RefundService is always started | |||||
super.onStart(server); | |||||
} | |||||
@Test public void testFreePayment () throws Exception { modelTest("payment/pay_free"); } | @Test public void testFreePayment () throws Exception { modelTest("payment/pay_free"); } | ||||
@Test public void testCodePayment () throws Exception { modelTest("payment/pay_code"); } | @Test public void testCodePayment () throws Exception { modelTest("payment/pay_code"); } | ||||
@Test public void testCreditPayment () throws Exception { modelTest("payment/pay_credit"); } | @Test public void testCreditPayment () throws Exception { modelTest("payment/pay_credit"); } | ||||
@@ -86,7 +86,7 @@ | |||||
"timezone": "EST", | "timezone": "EST", | ||||
"plan": "{{plans.[0].name}}", | "plan": "{{plans.[0].name}}", | ||||
"footprint": "US", | "footprint": "US", | ||||
"paymentMethod": { | |||||
"paymentMethodObject": { | |||||
"paymentMethodType": "credit", | "paymentMethodType": "credit", | ||||
"paymentInfo": "{{stripeToken}}" | "paymentInfo": "{{stripeToken}}" | ||||
} | } | ||||
@@ -158,6 +158,7 @@ | |||||
{"condition": "json.length === 1"}, | {"condition": "json.length === 1"}, | ||||
{"condition": "json[0].getAccountPlan() === '{{accountPlan.uuid}}'"}, | {"condition": "json[0].getAccountPlan() === '{{accountPlan.uuid}}'"}, | ||||
{"condition": "json[0].getAmount() === {{plans.[0].price}}"}, | {"condition": "json[0].getAmount() === {{plans.[0].price}}"}, | ||||
{"condition": "json[0].getType().name() === 'payment'"}, | |||||
{"condition": "json[0].getStatus().name() === 'success'"} | {"condition": "json[0].getStatus().name() === 'success'"} | ||||
] | ] | ||||
} | } | ||||
@@ -165,11 +166,13 @@ | |||||
{ | { | ||||
"comment": "verify successful payment has paid for the bill above", | "comment": "verify successful payment has paid for the bill above", | ||||
"request": { "uri": "me/payments/{{payments.[0].uuid}}/bills" }, | |||||
"request": { "uri": "me/plans/{{accountPlan.uuid}}/bills" }, | |||||
"response": { | "response": { | ||||
"check": [ | "check": [ | ||||
{"condition": "json.length === 1"}, | {"condition": "json.length === 1"}, | ||||
{"condition": "json[0].getUuid() === '{{bills.[0].uuid}}'"} | |||||
{"condition": "json[0].getUuid() === '{{bills.[0].uuid}}'"}, | |||||
{"condition": "json[0].getStatus().name() === 'paid'"}, | |||||
{"condition": "json[0].getRefundedAmount() === 0"} | |||||
] | ] | ||||
} | } | ||||
}, | }, | ||||
@@ -202,149 +205,72 @@ | |||||
} | } | ||||
}, | }, | ||||
{ | |||||
"comment": "verify no plans exist", | |||||
"request": { "uri": "me/plans" }, | |||||
"response": { | |||||
"check": [ {"condition": "json.length === 0"} ] | |||||
} | |||||
}, | |||||
{ | |||||
"comment": "add a second plan, using saved payment method", | |||||
"request": { | |||||
"uri": "me/plans", | |||||
"method": "put", | |||||
"entity": { | |||||
"name": "test-net2-{{rand 5}}", | |||||
"domain": "{{defaultDomain}}", | |||||
"locale": "en_US", | |||||
"timezone": "EST", | |||||
"plan": "{{plans.[0].name}}", | |||||
"footprint": "US", | |||||
"paymentMethod": { | |||||
"uuid": "{{savedPaymentMethod.uuid}}" | |||||
} | |||||
} | |||||
}, | |||||
"response": { | |||||
"store": "accountPlan2" | |||||
} | |||||
}, | |||||
{ | { | ||||
"before": "sleep 15s", | "before": "sleep 15s", | ||||
"comment": "verify we still have only one payment method", | |||||
"request": { "uri": "me/paymentMethods" }, | |||||
"response": { | |||||
"check": [ {"condition": "json.length === 1"} ] | |||||
} | |||||
}, | |||||
{ | |||||
"comment": "verify account plan payment info is same as used before", | |||||
"request": { "uri": "me/plans/{{accountPlan2.uuid}}/paymentMethod" }, | |||||
"response": { | |||||
"check": [ | |||||
{"condition": "json.getPaymentMethodType().name() === 'credit'"}, | |||||
{"condition": "json.getMaskedPaymentInfo() === 'XXXX-XXXX-XXXX-4242'"}, | |||||
{"condition": "json.getUuid() === '{{savedPaymentMethod.uuid}}'"} | |||||
] | |||||
} | |||||
}, | |||||
{ | |||||
"comment": "verify we now have two bills", | |||||
"request": { "uri": "me/bills" }, | |||||
"response": { | |||||
"check": [ | |||||
{"condition": "json.length === 2"} | |||||
] | |||||
} | |||||
}, | |||||
{ | |||||
"comment": "verify bill exists for new service, but price is zero", | |||||
"request": { "uri": "me/plans/{{accountPlan2.uuid}}/bills" }, | |||||
"response": { | |||||
"store": "plan2bills", | |||||
"check": [ | |||||
{"condition": "json.length === 1"}, | |||||
{"condition": "json[0].getPlan() === '{{plans.[0].uuid}}'"}, | |||||
{"condition": "json[0].getAccountPlan() === '{{accountPlan2.uuid}}'"}, | |||||
{"condition": "json[0].getQuantity() === 1"}, | |||||
{"condition": "json[0].getPrice() === 0"}, | |||||
{"condition": "json[0].getTotal() === 0"} | |||||
] | |||||
} | |||||
}, | |||||
{ | |||||
"comment": "verify we have still only made one successful payment", | |||||
"comment": "verify refund payment has been processed", | |||||
"request": { "uri": "me/payments" }, | "request": { "uri": "me/payments" }, | ||||
"response": { | "response": { | ||||
"check": [ | "check": [ | ||||
{"condition": "json.length === 1"}, | |||||
{"condition": "json.length === 2"}, | |||||
{"condition": "json[0].getBill() === '{{bills.[0].uuid}}'"}, | |||||
{"condition": "json[0].getType().name() === 'refund'"}, | |||||
{"condition": "json[0].getStatus().name() === 'success'"}, | {"condition": "json[0].getStatus().name() === 'success'"}, | ||||
{"condition": "json[0].getAmount() === {{plans.[0].price}}"} | |||||
{"condition": "json[0].getAmount() > 0"}, | |||||
{"condition": "json[0].getAmount() < {{bills.[0].total}}"}, | |||||
// not sure if current month has 28, 29, 30 or 31 days, so let's make some reasonable bounds | |||||
// we should only have a refund for all but one day | |||||
{"condition": "json[0].getAmount() > bills[0].getTotal() - (bills[0].getTotal() / (bills[0].daysInPeriod() - 1))"}, | |||||
{"condition": "json[0].getAmount() <= bills[0].getTotal() - (bills[0].getTotal() / bills[0].daysInPeriod())"} | |||||
] | ] | ||||
} | } | ||||
}, | }, | ||||
{ | { | ||||
"comment": "verify payment exists via plan and is successful, and price was zero", | |||||
"request": { "uri": "me/plans/{{accountPlan2.uuid}}/payments" }, | |||||
"comment": "verify refund is reflected on the original bill", | |||||
"request": { "uri": "me/plans/{{accountPlan.uuid}}/bills" }, | |||||
"response": { | "response": { | ||||
"store": "plan2payments", | |||||
"check": [ | "check": [ | ||||
{"condition": "json.length === 1"}, | {"condition": "json.length === 1"}, | ||||
{"condition": "json[0].getPlan() === '{{plans.[0].uuid}}'"}, | |||||
{"condition": "json[0].getAccountPlan() === '{{accountPlan2.uuid}}'"}, | |||||
{"condition": "json[0].getPaymentObject().getAmount() === {{plans.[0].price}}"}, | |||||
{"condition": "json[0].getPaymentObject().getStatus().name() === 'success'"}, | |||||
{"condition": "json[0].getBillObject().getPlan() === '{{plans.[0].uuid}}'"}, | |||||
{"condition": "json[0].getBillObject().getAccountPlan() === '{{accountPlan2.uuid}}'"}, | |||||
{"condition": "json[0].getBillObject().getQuantity() === 1"}, | |||||
{"condition": "json[0].getBillObject().getPrice() === 0"}, | |||||
{"condition": "json[0].getBillObject().getTotal() === 0"}, | |||||
{"condition": "json[0].getBillObject().getStatus().name() === 'paid'"} | |||||
{"condition": "json[0].getUuid() === '{{bills.[0].uuid}}'"}, | |||||
{"condition": "json[0].getStatus().name() === 'paid'"}, | |||||
{"condition": "json[0].getTotal() === {{plans.[0].price}}"}, | |||||
{"condition": "json[0].getRefundedAmount() > 0"}, | |||||
{"condition": "json[0].getRefundedAmount() < {{bills.[0].total}}"}, | |||||
// not sure if current month has 28, 29, 30 or 31 days, so let's make some reasonable bounds | |||||
// we should only have a refund for all but one day | |||||
{"condition": "json[0].getRefundedAmount() > bills[0].getTotal() - (bills[0].getTotal() / (bills[0].daysInPeriod() - 1))"}, | |||||
{"condition": "json[0].getRefundedAmount() <= bills[0].getTotal() - (bills[0].getTotal() / bills[0].daysInPeriod())"} | |||||
] | ] | ||||
} | } | ||||
}, | }, | ||||
{ | { | ||||
"comment": "verify successful payment has paid for the second bill (with zero total)", | |||||
"request": { "uri": "me/plans/{{accountPlan2.uuid}}/payments/{{plan2payments.[0].uuid}}/bill" }, | |||||
"comment": "verify no plans exist", | |||||
"request": { "uri": "me/plans" }, | |||||
"response": { | "response": { | ||||
"check": [ | |||||
{"condition": "json.getPlan() === '{{plans.[0].uuid}}'"}, | |||||
{"condition": "json.getAccountPlan() === '{{accountPlan2.uuid}}'"}, | |||||
{"condition": "json.getQuantity() === 1"}, | |||||
{"condition": "json.getPrice() === 0"}, | |||||
{"condition": "json.getTotal() === 0"} | |||||
] | |||||
"check": [ {"condition": "json.length === 0"} ] | |||||
} | } | ||||
}, | }, | ||||
{ | { | ||||
"comment": "add a third plan, this one will require a second payment since two plans are now active", | |||||
"comment": "add a second plan, using saved payment method", | |||||
"request": { | "request": { | ||||
"uri": "me/plans", | "uri": "me/plans", | ||||
"method": "put", | "method": "put", | ||||
"entity": { | "entity": { | ||||
"name": "test-net-{{rand 5}}", | |||||
"name": "test-net2-{{rand 5}}", | |||||
"domain": "{{defaultDomain}}", | "domain": "{{defaultDomain}}", | ||||
"locale": "en_US", | "locale": "en_US", | ||||
"timezone": "EST", | "timezone": "EST", | ||||
"plan": "{{plans.[0].name}}", | "plan": "{{plans.[0].name}}", | ||||
"footprint": "US", | "footprint": "US", | ||||
"paymentMethod": { | |||||
"paymentMethodObject": { | |||||
"uuid": "{{savedPaymentMethod.uuid}}" | "uuid": "{{savedPaymentMethod.uuid}}" | ||||
} | } | ||||
} | } | ||||
}, | }, | ||||
"response": { | "response": { | ||||
"store": "accountPlan3" | |||||
"store": "accountPlan2" | |||||
} | } | ||||
}, | }, | ||||
@@ -370,24 +296,24 @@ | |||||
}, | }, | ||||
{ | { | ||||
"comment": "verify we now have three bills", | |||||
"comment": "verify we now have two bills", | |||||
"request": { "uri": "me/bills" }, | "request": { "uri": "me/bills" }, | ||||
"response": { | "response": { | ||||
"check": [ | "check": [ | ||||
{"condition": "json.length === 3"} | |||||
{"condition": "json.length === 2"} | |||||
] | ] | ||||
} | } | ||||
}, | }, | ||||
{ | { | ||||
"comment": "verify bill exists for new service, and price is NOT zero", | |||||
"request": { "uri": "me/plans/{{accountPlan3.uuid}}/bills" }, | |||||
"comment": "verify bill exists for new service", | |||||
"request": { "uri": "me/plans/{{accountPlan2.uuid}}/bills" }, | |||||
"response": { | "response": { | ||||
"store": "plan2bills", | "store": "plan2bills", | ||||
"check": [ | "check": [ | ||||
{"condition": "json.length === 1"}, | {"condition": "json.length === 1"}, | ||||
{"condition": "json[0].getPlan() === '{{plans.[0].uuid}}'"}, | {"condition": "json[0].getPlan() === '{{plans.[0].uuid}}'"}, | ||||
{"condition": "json[0].getAccountPlan() === '{{accountPlan3.uuid}}'"}, | |||||
{"condition": "json[0].getAccountPlan() === '{{accountPlan2.uuid}}'"}, | |||||
{"condition": "json[0].getQuantity() === 1"}, | {"condition": "json[0].getQuantity() === 1"}, | ||||
{"condition": "json[0].getPrice() === {{plans.[0].price}}"}, | {"condition": "json[0].getPrice() === {{plans.[0].price}}"}, | ||||
{"condition": "json[0].getTotal() === {{plans.[0].price}}"} | {"condition": "json[0].getTotal() === {{plans.[0].price}}"} | ||||
@@ -396,68 +322,37 @@ | |||||
}, | }, | ||||
{ | { | ||||
"comment": "verify we have now made two successful payments", | |||||
"comment": "verify we have now made two successful payments (plus one refund)", | |||||
"request": { "uri": "me/payments" }, | "request": { "uri": "me/payments" }, | ||||
"response": { | "response": { | ||||
"check": [ | "check": [ | ||||
{"condition": "json.length === 2"}, | |||||
{"condition": "json.length === 3"}, | |||||
{"condition": "json[0].getStatus().name() === 'success'"}, | {"condition": "json[0].getStatus().name() === 'success'"}, | ||||
{"condition": "json[0].getAmount() === {{plans.[0].price}}"}, | |||||
{"condition": "json[1].getStatus().name() === 'success'"}, | |||||
{"condition": "json[1].getAmount() === {{plans.[0].price}}"} | |||||
{"condition": "json[0].getAccountPlan() === '{{accountPlan2.uuid}}'"}, | |||||
{"condition": "json[0].getAmount() === {{plans.[0].price}}"} | |||||
] | ] | ||||
} | } | ||||
}, | }, | ||||
{ | { | ||||
"comment": "verify payment exists via plan and is successful, and price was NOT zero", | |||||
"request": { "uri": "me/plans/{{accountPlan3.uuid}}/payments" }, | |||||
"comment": "verify payment exists via plan and is successful", | |||||
"request": { "uri": "me/plans/{{accountPlan2.uuid}}/payments" }, | |||||
"response": { | "response": { | ||||
"store": "plan3payments", | |||||
"store": "plan2payments", | |||||
"check": [ | "check": [ | ||||
{"condition": "json.length === 1"}, | {"condition": "json.length === 1"}, | ||||
{"condition": "json[0].getPlan() === '{{plans.[0].uuid}}'"}, | {"condition": "json[0].getPlan() === '{{plans.[0].uuid}}'"}, | ||||
{"condition": "json[0].getAccountPlan() === '{{accountPlan3.uuid}}'"}, | |||||
{"condition": "json[0].getPaymentObject().getAmount() === {{plans.[0].price}}"}, | |||||
{"condition": "json[0].getPaymentObject().getStatus().name() === 'success'"}, | |||||
{"condition": "json[0].getAccountPlan() === '{{accountPlan2.uuid}}'"}, | |||||
{"condition": "json[0].getAmount() === {{plans.[0].price}}"}, | |||||
{"condition": "json[0].getStatus().name() === 'success'"}, | |||||
{"condition": "json[0].getBillObject().getPlan() === '{{plans.[0].uuid}}'"}, | {"condition": "json[0].getBillObject().getPlan() === '{{plans.[0].uuid}}'"}, | ||||
{"condition": "json[0].getBillObject().getAccountPlan() === '{{accountPlan3.uuid}}'"}, | |||||
{"condition": "json[0].getBillObject().getAccountPlan() === '{{accountPlan2.uuid}}'"}, | |||||
{"condition": "json[0].getBillObject().getQuantity() === 1"}, | {"condition": "json[0].getBillObject().getQuantity() === 1"}, | ||||
{"condition": "json[0].getBillObject().getPrice() === {{plans.[0].price}}"}, | {"condition": "json[0].getBillObject().getPrice() === {{plans.[0].price}}"}, | ||||
{"condition": "json[0].getBillObject().getTotal() === {{plans.[0].price}}"}, | {"condition": "json[0].getBillObject().getTotal() === {{plans.[0].price}}"}, | ||||
{"condition": "json[0].getBillObject().getStatus().name() === 'paid'"} | {"condition": "json[0].getBillObject().getStatus().name() === 'paid'"} | ||||
] | ] | ||||
} | } | ||||
}, | |||||
{ | |||||
"comment": "verify successful payment has paid for the third bill (with non-zero total)", | |||||
"request": { "uri": "me/plans/{{accountPlan3.uuid}}/payments/{{plan3payments.[0].uuid}}/bill" }, | |||||
"response": { | |||||
"check": [ | |||||
{"condition": "json.getPlan() === '{{plans.[0].uuid}}'"}, | |||||
{"condition": "json.getAccountPlan() === '{{accountPlan3.uuid}}'"}, | |||||
{"condition": "json.getQuantity() === 1"}, | |||||
{"condition": "json.getPrice() === {{plans.[0].price}}"}, | |||||
{"condition": "json.getTotal() === {{plans.[0].price}}"} | |||||
] | |||||
} | |||||
}, | |||||
{ | |||||
"comment": "delete third network", | |||||
"request": { | |||||
"method": "delete", | |||||
"uri": "me/networks/{{accountPlan3.network}}" | |||||
} | |||||
}, | |||||
{ | |||||
"comment": "delete third plan", | |||||
"request": { | |||||
"method": "delete", | |||||
"uri": "me/plans/{{accountPlan3.uuid}}" | |||||
} | |||||
} | } | ||||
// todo: fast-forward 32 days, trigger BillGenerator | // todo: fast-forward 32 days, trigger BillGenerator | ||||