diff --git a/bubble-server/src/main/java/bubble/cloud/payment/PaymentDriverBase.java b/bubble-server/src/main/java/bubble/cloud/payment/PaymentDriverBase.java index 28ef7e46..68086ce4 100644 --- a/bubble-server/src/main/java/bubble/cloud/payment/PaymentDriverBase.java +++ b/bubble-server/src/main/java/bubble/cloud/payment/PaymentDriverBase.java @@ -7,6 +7,8 @@ import bubble.model.bill.*; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import java.util.List; + import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx; @Slf4j @@ -60,7 +62,7 @@ public abstract class PaymentDriverBase extends CloudServiceDriverBase imp return bill; } - @Override public boolean authorize(BubblePlan plan, AccountPaymentMethod paymentMethod) { + @Override public boolean authorize(BubblePlan plan, String accountPlanUuid, AccountPaymentMethod paymentMethod) { return true; } @@ -131,7 +133,14 @@ public abstract class PaymentDriverBase extends CloudServiceDriverBase imp // mark the bill as paid, enable the plan 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 unpaidBills = billDAO.findUnpaidByAccountPlan(accountPlan.getUuid()); + if (unpaidBills.isEmpty()) { + accountPlanDAO.update(accountPlan.setEnabled(true)); + } else { + accountPlanDAO.update(accountPlan.setEnabled(false)); + } return accountPayment; } @@ -181,7 +190,11 @@ public abstract class PaymentDriverBase extends CloudServiceDriverBase imp } // 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; try { diff --git a/bubble-server/src/main/java/bubble/cloud/payment/PaymentServiceDriver.java b/bubble-server/src/main/java/bubble/cloud/payment/PaymentServiceDriver.java index f6801768..8b16e0dd 100644 --- a/bubble-server/src/main/java/bubble/cloud/payment/PaymentServiceDriver.java +++ b/bubble-server/src/main/java/bubble/cloud/payment/PaymentServiceDriver.java @@ -17,7 +17,7 @@ public interface PaymentServiceDriver { default PaymentValidationResult claim(AccountPaymentMethod paymentMethod) { 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); diff --git a/bubble-server/src/main/java/bubble/cloud/payment/delegate/DelegatedPaymentDriver.java b/bubble-server/src/main/java/bubble/cloud/payment/delegate/DelegatedPaymentDriver.java index d8dc16e9..9c8d83e3 100644 --- a/bubble-server/src/main/java/bubble/cloud/payment/delegate/DelegatedPaymentDriver.java +++ b/bubble-server/src/main/java/bubble/cloud/payment/delegate/DelegatedPaymentDriver.java @@ -52,12 +52,13 @@ public class DelegatedPaymentDriver extends DelegatedCloudServiceDriverBase impl 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 PaymentResult result = notificationService.notifySync(delegate, payment_driver_authorize, new PaymentNotification() .setCloud(cloud.getName()) .setPlanUuid(plan.getUuid()) + .setAccountPlanUuid(accountPlanUuid) .setPaymentMethodUuid(paymentMethod.getUuid())); return processResult(result); } @@ -89,7 +90,7 @@ public class DelegatedPaymentDriver extends DelegatedCloudServiceDriverBase impl if (result.hasViolations()) { throw invalidEx(result.violationList()); } - if (result.hasError()) return die("authorize: "+result.getError()); + if (result.hasError()) return die("processResult: "+result.getError()); return false; } diff --git a/bubble-server/src/main/java/bubble/cloud/payment/stripe/StripePaymentDriver.java b/bubble-server/src/main/java/bubble/cloud/payment/stripe/StripePaymentDriver.java index 9cec923a..597391bc 100644 --- a/bubble-server/src/main/java/bubble/cloud/payment/stripe/StripePaymentDriver.java +++ b/bubble-server/src/main/java/bubble/cloud/payment/stripe/StripePaymentDriver.java @@ -126,6 +126,7 @@ public class StripePaymentDriver extends PaymentDriverBase { @@ -36,11 +38,18 @@ public class AccountPlanDAO extends AccountOwnedEntityDAO { } public List findByAccountAndNotDeleted(String account) { - return findByFields("account", account, "deleted", false); + return findByFields("account", account, "deleted", null); } public List findByAccountAndPaymentMethodAndNotDeleted(String account, String paymentMethod) { - return findByFields("account", account, "paymentMethod", paymentMethod, "deleted", false); + return findByFields("account", account, "paymentMethod", paymentMethod, "deleted", null); + } + + public List findByDeletedAndNotClosed() { + return list(criteria().add(and( + isNotNull("deleted"), + eq("closed", false) + ))); } @Override public Object preCreate(AccountPlan accountPlan) { @@ -62,7 +71,8 @@ public class AccountPlanDAO extends AccountOwnedEntityDAO { } if (paymentDriver.getPaymentMethodType().requiresAuth()) { 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()); } @@ -81,6 +91,8 @@ public class AccountPlanDAO extends AccountOwnedEntityDAO { .setPrice(plan.getPrice()) .setCurrency(plan.getCurrency()) .setPeriod(plan.getPeriod().currentPeriod()) + .setPeriodStart(plan.getPeriod().getFirstPeriodStart()) + .setPeriodEnd(plan.getPeriod().getFirstPeriodEnd()) .setQuantity(1L) .setType(BillItemType.compute) .setStatus(BillStatus.unpaid)); @@ -106,7 +118,7 @@ public class AccountPlanDAO extends AccountOwnedEntityDAO { if (network != null && network.getState() != BubbleNetworkState.stopped) { throw invalidEx("err.accountPlan.stopNetworkBeforeDeleting"); } - update(accountPlan.setDeleted(true).setEnabled(false)); + update(accountPlan.setDeleted(now()).setEnabled(false)); if (configuration.paymentsEnabled()) { refundService.processRefunds(); } diff --git a/bubble-server/src/main/java/bubble/dao/bill/BillDAO.java b/bubble-server/src/main/java/bubble/dao/bill/BillDAO.java index 2cf3c897..cd581150 100644 --- a/bubble-server/src/main/java/bubble/dao/bill/BillDAO.java +++ b/bubble-server/src/main/java/bubble/dao/bill/BillDAO.java @@ -2,6 +2,7 @@ package bubble.dao.bill; import bubble.dao.account.AccountOwnedEntityDAO; import bubble.model.bill.Bill; +import bubble.model.bill.BillStatus; import org.hibernate.criterion.Order; import org.springframework.stereotype.Repository; @@ -22,4 +23,8 @@ public class BillDAO extends AccountOwnedEntityDAO { return bills.isEmpty() ? null : bills.get(0); } + public List findUnpaidByAccountPlan(String accountPlanUuid) { + return findByFields("accountPlan", accountPlanUuid, "status", BillStatus.unpaid); + } + } diff --git a/bubble-server/src/main/java/bubble/model/bill/AccountPlan.java b/bubble-server/src/main/java/bubble/model/bill/AccountPlan.java index 5b459dff..b155110c 100644 --- a/bubble-server/src/main/java/bubble/model/bill/AccountPlan.java +++ b/bubble-server/src/main/java/bubble/model/bill/AccountPlan.java @@ -38,6 +38,8 @@ public class AccountPlan extends IdentifiableBase implements HasAccount { @SuppressWarnings("unused") public AccountPlan (AccountPlan other) { copy(this, other, CREATE_FIELDS); } + @Override public void beforeCreate() { if (!hasUuid()) initUuid(); } + // mirrors network name @Size(max=100, message="err.name.length") @Column(length=100, nullable=false) @@ -67,13 +69,12 @@ public class AccountPlan extends IdentifiableBase implements HasAccount { @Getter @Setter private Boolean enabled = false; 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(); } @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 notClosed() { return !closed(); } diff --git a/bubble-server/src/main/java/bubble/model/bill/Bill.java b/bubble-server/src/main/java/bubble/model/bill/Bill.java index 9b30cfe8..e04e83e0 100644 --- a/bubble-server/src/main/java/bubble/model/bill/Bill.java +++ b/bubble-server/src/main/java/bubble/model/bill/Bill.java @@ -51,9 +51,17 @@ public class Bill extends IdentifiableBase implements HasAccountNoName { @Column(nullable=false, updatable=false, length=20) @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; + @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") @Getter @Setter private Long quantity = 0L; diff --git a/bubble-server/src/main/java/bubble/model/bill/BillPeriod.java b/bubble-server/src/main/java/bubble/model/bill/BillPeriod.java index 03c2b2a5..6cd0efec 100644 --- a/bubble-server/src/main/java/bubble/model/bill/BillPeriod.java +++ b/bubble-server/src/main/java/bubble/model/bill/BillPeriod.java @@ -5,6 +5,9 @@ import com.fasterxml.jackson.annotation.JsonCreator; import lombok.AllArgsConstructor; import lombok.Getter; 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 static bubble.ApiConstants.enumFromString; @@ -17,17 +20,30 @@ public enum BillPeriod { monthly (DATE_FORMAT_YYYY_MM); + public static final DateTimeFormatter BILL_START_END_FORMAT = DateTimeFormat.forPattern("yyyy-MM-dd"); + @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); } + 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 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)); + } } diff --git a/bubble-server/src/main/java/bubble/model/bill/period/BillPeriodDriver.java b/bubble-server/src/main/java/bubble/model/bill/period/BillPeriodDriver.java index 227d3eb9..183c01ac 100644 --- a/bubble-server/src/main/java/bubble/model/bill/period/BillPeriodDriver.java +++ b/bubble-server/src/main/java/bubble/model/bill/period/BillPeriodDriver.java @@ -1,11 +1,13 @@ 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 { - long calculateRefund(long planStart, String period, long total); + long calculateRefund(Bill bill, AccountPlan plan); - DateTime nextPeriod(); + DurationFieldType getDurationFieldType(); } diff --git a/bubble-server/src/main/java/bubble/model/bill/period/BillPeriodDriver_monthly.java b/bubble-server/src/main/java/bubble/model/bill/period/BillPeriodDriver_monthly.java index 16e51bc0..71a7d163 100644 --- a/bubble-server/src/main/java/bubble/model/bill/period/BillPeriodDriver_monthly.java +++ b/bubble-server/src/main/java/bubble/model/bill/period/BillPeriodDriver_monthly.java @@ -1,52 +1,37 @@ 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 org.joda.time.DateTime; import org.joda.time.Days; -import org.joda.time.LocalDate; +import org.joda.time.DurationFieldType; 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.now; -import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx; @Slf4j 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(); - } } diff --git a/bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_authorize.java b/bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_authorize.java index 636f8f53..deb7a6f4 100644 --- a/bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_authorize.java +++ b/bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_authorize.java @@ -1,7 +1,7 @@ package bubble.notify.payment; +import bubble.cloud.payment.PaymentServiceDriver; import bubble.dao.bill.AccountPaymentMethodDAO; -import bubble.dao.bill.AccountPlanDAO; import bubble.dao.bill.BubblePlanDAO; import bubble.model.bill.AccountPaymentMethod; import bubble.model.bill.BubblePlan; @@ -12,12 +12,12 @@ public class NotificationHandler_payment_driver_authorize extends NotificationHa @Autowired private BubblePlanDAO planDAO; @Autowired private AccountPaymentMethodDAO paymentMethodDAO; - @Autowired private AccountPlanDAO accountPlanDAO; @Override public boolean handlePaymentRequest(PaymentNotification paymentNotification, CloudService paymentService) { final BubblePlan plan = planDAO.findByUuid(paymentNotification.getPlanUuid()); 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); } } diff --git a/bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java b/bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java index 7c14ca9e..5b4d7fdb 100644 --- a/bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java +++ b/bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java @@ -47,11 +47,6 @@ public class AccountPlansResource extends AccountOwnedResource list(ContainerRequest ctx) { return getDao().findByAccountAndNotDeleted(account.getUuid()); } diff --git a/bubble-server/src/main/java/bubble/server/listener/NodeInitializerListener.java b/bubble-server/src/main/java/bubble/server/listener/NodeInitializerListener.java index 61d0e599..b4199c5b 100644 --- a/bubble-server/src/main/java/bubble/server/listener/NodeInitializerListener.java +++ b/bubble-server/src/main/java/bubble/server/listener/NodeInitializerListener.java @@ -9,6 +9,7 @@ import bubble.dao.cloud.CloudServiceDAO; import bubble.model.cloud.BubbleNode; import bubble.model.cloud.BubbleNodeKey; import bubble.server.BubbleConfiguration; +import bubble.service.bill.RefundService; import bubble.service.boot.SageHelloService; import lombok.extern.slf4j.Slf4j; import org.cobbzilla.util.string.StringUtil; @@ -109,6 +110,12 @@ public class NodeInitializerListener extends RestServerLifecycleListenerBase 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()); + } + } } } diff --git a/bubble-server/src/test/java/bubble/test/BubbleModelTestBase.java b/bubble-server/src/test/java/bubble/test/BubbleModelTestBase.java index 3ed8f4d6..c31ffd7b 100644 --- a/bubble-server/src/test/java/bubble/test/BubbleModelTestBase.java +++ b/bubble-server/src/test/java/bubble/test/BubbleModelTestBase.java @@ -30,8 +30,6 @@ import static org.cobbzilla.util.system.CommandShell.loadShellExportsOrDie; @Slf4j public abstract class BubbleModelTestBase extends ApiModelTestBase { - public static final String[] SQL_POST_SCRIPTS = {"models/constraints.sql"}; - public static final List TEST_LIFECYCLE_LISTENERS = asList(new RestServerLifecycleListener[] { new NodeInitializerListener() }); diff --git a/bubble-server/src/test/java/bubble/test/PaymentTest.java b/bubble-server/src/test/java/bubble/test/PaymentTest.java index 086571db..de3ba824 100644 --- a/bubble-server/src/test/java/bubble/test/PaymentTest.java +++ b/bubble-server/src/test/java/bubble/test/PaymentTest.java @@ -1,6 +1,7 @@ package bubble.test; import bubble.server.BubbleConfiguration; +import bubble.service.bill.RefundService; import lombok.extern.slf4j.Slf4j; import org.cobbzilla.wizard.server.RestServer; import org.junit.Test; @@ -18,6 +19,12 @@ public class PaymentTest extends ActivatedBubbleModelTestBase { super.beforeStart(server); } + @Override public void onStart(RestServer 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 testCodePayment () throws Exception { modelTest("payment/pay_code"); } @Test public void testCreditPayment () throws Exception { modelTest("payment/pay_credit"); } diff --git a/bubble-server/src/test/resources/models/tests/payment/pay_credit_multi_start_stop.json b/bubble-server/src/test/resources/models/tests/payment/pay_credit_multi_start_stop.json index 6f797af9..0ae7ab4b 100644 --- a/bubble-server/src/test/resources/models/tests/payment/pay_credit_multi_start_stop.json +++ b/bubble-server/src/test/resources/models/tests/payment/pay_credit_multi_start_stop.json @@ -86,7 +86,7 @@ "timezone": "EST", "plan": "{{plans.[0].name}}", "footprint": "US", - "paymentMethod": { + "paymentMethodObject": { "paymentMethodType": "credit", "paymentInfo": "{{stripeToken}}" } @@ -158,6 +158,7 @@ {"condition": "json.length === 1"}, {"condition": "json[0].getAccountPlan() === '{{accountPlan.uuid}}'"}, {"condition": "json[0].getAmount() === {{plans.[0].price}}"}, + {"condition": "json[0].getType().name() === 'payment'"}, {"condition": "json[0].getStatus().name() === 'success'"} ] } @@ -165,11 +166,13 @@ { "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": { "check": [ {"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", - "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" }, "response": { "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].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": { - "store": "plan2payments", "check": [ {"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": { - "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": { "uri": "me/plans", "method": "put", "entity": { - "name": "test-net-{{rand 5}}", + "name": "test-net2-{{rand 5}}", "domain": "{{defaultDomain}}", "locale": "en_US", "timezone": "EST", "plan": "{{plans.[0].name}}", "footprint": "US", - "paymentMethod": { + "paymentMethodObject": { "uuid": "{{savedPaymentMethod.uuid}}" } } }, "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" }, "response": { "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": { "store": "plan2bills", "check": [ {"condition": "json.length === 1"}, {"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].getPrice() === {{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" }, "response": { "check": [ - {"condition": "json.length === 2"}, + {"condition": "json.length === 3"}, {"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": { - "store": "plan3payments", + "store": "plan2payments", "check": [ {"condition": "json.length === 1"}, {"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().getAccountPlan() === '{{accountPlan3.uuid}}'"}, + {"condition": "json[0].getBillObject().getAccountPlan() === '{{accountPlan2.uuid}}'"}, {"condition": "json[0].getBillObject().getQuantity() === 1"}, {"condition": "json[0].getBillObject().getPrice() === {{plans.[0].price}}"}, {"condition": "json[0].getBillObject().getTotal() === {{plans.[0].price}}"}, {"condition": "json[0].getBillObject().getStatus().name() === 'paid'"} ] } - }, - - { - "comment": "verify 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