Ver código fonte

WIP. credit tests now passing, with refunds working

tags/v0.1.6
Jonathan Cobb 4 anos atrás
pai
commit
9bfb7468af
18 arquivos alterados com 207 adições e 226 exclusões
  1. +16
    -3
      bubble-server/src/main/java/bubble/cloud/payment/PaymentDriverBase.java
  2. +1
    -1
      bubble-server/src/main/java/bubble/cloud/payment/PaymentServiceDriver.java
  3. +3
    -2
      bubble-server/src/main/java/bubble/cloud/payment/delegate/DelegatedPaymentDriver.java
  4. +3
    -2
      bubble-server/src/main/java/bubble/cloud/payment/stripe/StripePaymentDriver.java
  5. +16
    -4
      bubble-server/src/main/java/bubble/dao/bill/AccountPlanDAO.java
  6. +5
    -0
      bubble-server/src/main/java/bubble/dao/bill/BillDAO.java
  7. +5
    -4
      bubble-server/src/main/java/bubble/model/bill/AccountPlan.java
  8. +9
    -1
      bubble-server/src/main/java/bubble/model/bill/Bill.java
  9. +20
    -4
      bubble-server/src/main/java/bubble/model/bill/BillPeriod.java
  10. +5
    -3
      bubble-server/src/main/java/bubble/model/bill/period/BillPeriodDriver.java
  11. +18
    -33
      bubble-server/src/main/java/bubble/model/bill/period/BillPeriodDriver_monthly.java
  12. +3
    -3
      bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_authorize.java
  13. +0
    -5
      bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java
  14. +7
    -0
      bubble-server/src/main/java/bubble/server/listener/NodeInitializerListener.java
  15. +39
    -4
      bubble-server/src/main/java/bubble/service/bill/RefundService.java
  16. +0
    -2
      bubble-server/src/test/java/bubble/test/BubbleModelTestBase.java
  17. +7
    -0
      bubble-server/src/test/java/bubble/test/PaymentTest.java
  18. +50
    -155
      bubble-server/src/test/resources/models/tests/payment/pay_credit_multi_start_stop.json

+ 16
- 3
bubble-server/src/main/java/bubble/cloud/payment/PaymentDriverBase.java Ver arquivo

@@ -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<T> extends CloudServiceDriverBase<T> 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<T> extends CloudServiceDriverBase<T> 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<Bill> 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<T> extends CloudServiceDriverBase<T> 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 {


+ 1
- 1
bubble-server/src/main/java/bubble/cloud/payment/PaymentServiceDriver.java Ver arquivo

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



+ 3
- 2
bubble-server/src/main/java/bubble/cloud/payment/delegate/DelegatedPaymentDriver.java Ver arquivo

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



+ 3
- 2
bubble-server/src/main/java/bubble/cloud/payment/stripe/StripePaymentDriver.java Ver arquivo

@@ -126,6 +126,7 @@ public class StripePaymentDriver extends PaymentDriverBase<StripePaymentDriverCo
}

@Override public boolean authorize(BubblePlan plan,
String accountPlanUuid,
AccountPaymentMethod paymentMethod) {
final String planUuid = plan.getUuid();
final String paymentMethodUuid = paymentMethod.getUuid();
@@ -141,7 +142,7 @@ public class StripePaymentDriver extends PaymentDriverBase<StripePaymentDriverCo
chargeParams.put("statement_descriptor", plan.chargeDescription());
chargeParams.put("capture", false);
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);
if (chargeId != null) {
log.warn("authorize: already authorized: "+authCacheKey);
@@ -208,7 +209,7 @@ public class StripePaymentDriver extends PaymentDriverBase<StripePaymentDriverCo
final RedisService authCache = getAuthCache();
final RedisService chargeCache = getChargeCache();

final String authCacheKey = getAuthCacheKey(plan.getUuid(), paymentMethodUuid);
final String authCacheKey = getAuthCacheKey(accountPlanUuid, paymentMethodUuid);
try {
final String charged = chargeCache.get(billUuid);
if (charged != null) {


+ 16
- 4
bubble-server/src/main/java/bubble/dao/bill/AccountPlanDAO.java Ver arquivo

@@ -18,8 +18,10 @@ import java.util.List;

import static java.util.concurrent.TimeUnit.SECONDS;
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.wizard.resources.ResourceUtil.invalidEx;
import static org.hibernate.criterion.Restrictions.*;

@Repository
public class AccountPlanDAO extends AccountOwnedEntityDAO<AccountPlan> {
@@ -36,11 +38,18 @@ public class AccountPlanDAO extends AccountOwnedEntityDAO<AccountPlan> {
}

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) {
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) {
@@ -62,7 +71,8 @@ public class AccountPlanDAO extends AccountOwnedEntityDAO<AccountPlan> {
}
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<AccountPlan> {
.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<AccountPlan> {
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();
}


+ 5
- 0
bubble-server/src/main/java/bubble/dao/bill/BillDAO.java Ver arquivo

@@ -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<Bill> {
return bills.isEmpty() ? null : bills.get(0);
}

public List<Bill> findUnpaidByAccountPlan(String accountPlanUuid) {
return findByFields("accountPlan", accountPlanUuid, "status", BillStatus.unpaid);
}

}

+ 5
- 4
bubble-server/src/main/java/bubble/model/bill/AccountPlan.java Ver arquivo

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



+ 9
- 1
bubble-server/src/main/java/bubble/model/bill/Bill.java Ver arquivo

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



+ 20
- 4
bubble-server/src/main/java/bubble/model/bill/BillPeriod.java Ver arquivo

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

}

+ 5
- 3
bubble-server/src/main/java/bubble/model/bill/period/BillPeriodDriver.java Ver arquivo

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

}

+ 18
- 33
bubble-server/src/main/java/bubble/model/bill/period/BillPeriodDriver_monthly.java Ver arquivo

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

+ 3
- 3
bubble-server/src/main/java/bubble/notify/payment/NotificationHandler_payment_driver_authorize.java Ver arquivo

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

}

+ 0
- 5
bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java Ver arquivo

@@ -47,11 +47,6 @@ public class AccountPlansResource extends AccountOwnedResource<AccountPlan, Acco

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) {
return getDao().findByAccountAndNotDeleted(account.getUuid());
}


+ 7
- 0
bubble-server/src/main/java/bubble/server/listener/NodeInitializerListener.java Ver arquivo

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

+ 39
- 4
bubble-server/src/main/java/bubble/service/bill/RefundService.java Ver arquivo

@@ -1,16 +1,51 @@
package bubble.service.bill;

import bubble.cloud.payment.PaymentServiceDriver;
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.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 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());
}
}
}

}

+ 0
- 2
bubble-server/src/test/java/bubble/test/BubbleModelTestBase.java Ver arquivo

@@ -30,8 +30,6 @@ import static org.cobbzilla.util.system.CommandShell.loadShellExportsOrDie;
@Slf4j
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[] {
new NodeInitializerListener()
});


+ 7
- 0
bubble-server/src/test/java/bubble/test/PaymentTest.java Ver arquivo

@@ -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<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 testCodePayment () throws Exception { modelTest("payment/pay_code"); }
@Test public void testCreditPayment () throws Exception { modelTest("payment/pay_credit"); }


+ 50
- 155
bubble-server/src/test/resources/models/tests/payment/pay_credit_multi_start_stop.json Ver arquivo

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


Carregando…
Cancelar
Salvar