Browse Source

verify 2nd month payment is via credit card, does not use promotion

tags/v0.7.2
Jonathan Cobb 4 years ago
parent
commit
a15024cb21
11 changed files with 266 additions and 53 deletions
  1. +2
    -43
      bubble-server/src/main/java/bubble/cloud/payment/PaymentDriverBase.java
  2. +1
    -0
      bubble-server/src/main/java/bubble/cloud/payment/firstMonthFree/FirstMonthFreePaymentDriver.java
  3. +6
    -0
      bubble-server/src/main/java/bubble/cloud/payment/stripe/StripePaymentDriver.java
  4. +5
    -1
      bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java
  5. +7
    -4
      bubble-server/src/main/java/bubble/service/bill/BillingService.java
  6. +9
    -2
      bubble-server/src/main/java/bubble/service/cloud/NetworkMonitorService.java
  7. +1
    -1
      bubble-server/src/main/resources/bubble-config.yml
  8. +1
    -0
      bubble-server/src/test/java/bubble/test/BubbleApiRunnerListener.java
  9. +232
    -0
      bubble-server/src/test/resources/models/tests/promo/first_month_free.json
  10. +1
    -1
      bubble-server/src/test/resources/test-bubble-config.yml
  11. +1
    -1
      utils/cobbzilla-wizard

+ 2
- 43
bubble-server/src/main/java/bubble/cloud/payment/PaymentDriverBase.java View File

@@ -14,8 +14,8 @@ import java.util.Map;
import java.util.TreeMap;

import static bubble.model.bill.PaymentMethodType.promotional_credit;
import static org.cobbzilla.util.daemon.ZillaRuntime.*;
import static org.cobbzilla.wizard.model.IdentifiableBase.CTIME_ASC;
import static org.cobbzilla.util.daemon.ZillaRuntime.die;
import static org.cobbzilla.util.daemon.ZillaRuntime.shortError;
import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx;

@Slf4j
@@ -70,47 +70,6 @@ public abstract class PaymentDriverBase<T> extends CloudServiceDriverBase<T> imp
return bill;
}

protected long applyPromotions(String accountPlanUuid, AccountPaymentMethod paymentMethod, long price) {
// cannot apply a promotion to a promotion -- should never happen
if (getPaymentMethodType() == promotional_credit) {
log.warn("applyPromotions: cannot apply promotions to a promotion");
return price;
}

final List<AccountPaymentMethod> promos = paymentMethodDAO.findByAccountAndPromoAndNotDeleted(paymentMethod.getAccount());
if (!empty(promos)) {
// sort oldest first, this ensures default promotions (like first month free) get applied before referral promotions
promos.sort(CTIME_ASC);
Promotion selectedPromo = null;
for (AccountPaymentMethod apm : promos) {
final Promotion promo = promotionDAO.findByUuid(apm.getPromotion());
if (promo != null && promo.active()) {

}
}
}


final Bill bill = billDAO.findOldestUnpaidBillByAccountPlan(accountPlanUuid);
final long creditApplied;
if (bill == null) {
log.warn("No unpaid bills for account "+paymentMethod.getAccount()+" and accountPlanUuid="+accountPlanUuid+", no credit applied");
creditApplied = 0;
} else {
final AccountPayment promoPayment = accountPaymentDAO.findByAccountAndAccountPlanAndBillAndCreditAppliedSuccess(paymentMethod.getAccount(), accountPlanUuid, bill.getUuid());
if (promoPayment != null) {
creditApplied = promoPayment.getAmount();
} else {
creditApplied = 0;
}
}
if (creditApplied >= price) {
log.info("getChargeAmount: credit applied ("+creditApplied+") exceeds price "+price+", no charge due");
return 0;
}
return price - creditApplied;
}

@Override public boolean authorize(BubblePlan plan, String accountPlanUuid, AccountPaymentMethod paymentMethod) {
return true;
}


+ 1
- 0
bubble-server/src/main/java/bubble/cloud/payment/firstMonthFree/FirstMonthFreePaymentDriver.java View File

@@ -61,6 +61,7 @@ public class FirstMonthFreePaymentDriver extends PaymentDriverBase<FirstMonthPay
Bill bill,
long chargeAmount) {
// mark deleted so it will not be found/applied for future transactions
log.info("charge: applying promotion: "+paymentMethod.getPromotion()+" via AccountPaymentMethod: "+paymentMethod.getUuid());
paymentMethodDAO.update(paymentMethod.setDeleted());
return FIRST_MONTH_FREE_INFO;
}


+ 6
- 0
bubble-server/src/main/java/bubble/cloud/payment/stripe/StripePaymentDriver.java View File

@@ -49,6 +49,12 @@ public class StripePaymentDriver extends PaymentDriverBase<StripePaymentDriverCo
@Getter(lazy=true) private final RedisService chargeCache = redisService.prefixNamespace(SIMPLE_NAME +"_charge");
@Getter(lazy=true) private final RedisService refundCache = redisService.prefixNamespace(SIMPLE_NAME +"_refund");

public void flushCaches () {
getAuthCache().flush();
getChargeCache().flush();
getRefundCache().flush();
}

private static final AtomicReference<String> setupDone = new AtomicReference<>(null);

@Override public void postSetup() {


+ 5
- 1
bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java View File

@@ -224,7 +224,11 @@ public class AccountPlansResource extends AccountOwnedResource<AccountPlan, Acco
} else {
final PromotionalPaymentServiceDriver promoPaymentDriver = (PromotionalPaymentServiceDriver) promoDriver;
if (!promoPaymentDriver.applyPromo(promo, caller)) {
errors.addViolation("err.promoCode.notApplied");
if (request.hasPromoCode()) {
errors.addViolation("err.promoCode.notApplied");
} else {
log.warn("setReferences: promo not applied: "+promo.getName());
}
}
}
}


+ 7
- 4
bubble-server/src/main/java/bubble/service/bill/BillingService.java View File

@@ -29,6 +29,7 @@ import java.util.List;
import static java.util.concurrent.TimeUnit.HOURS;
import static org.cobbzilla.util.daemon.ZillaRuntime.die;
import static org.cobbzilla.util.daemon.ZillaRuntime.now;
import static org.cobbzilla.wizard.server.RestServerBase.reportError;

@Service @Slf4j
public class BillingService extends SimpleDaemon {
@@ -58,8 +59,9 @@ public class BillingService extends SimpleDaemon {

final BubblePlan plan = planDAO.findByUuid(accountPlan.getPlan());
if (plan == null) {
// todo: this is really bad -- notify admin
log.error("billPlan: plan not found ("+accountPlan.getPlan()+") for accountPlan: "+accountPlan.getUuid());
final String msg = "process: plan not found (" + accountPlan.getPlan() + ") for accountPlan: " + accountPlan.getUuid();
log.error(msg);
reportError("BillingService: "+msg);
continue;
}

@@ -97,8 +99,9 @@ public class BillingService extends SimpleDaemon {
try {
networkService.stopNetwork(network);
} catch (Exception e) {
// todo: notify admin, requires intervention
log.error("process: error stopping network due to non-payment: "+network.getUuid());
final String msg = "process: error stopping network due to non-payment: " + network.getUuid();
log.error(msg);
reportError("BillingService: "+msg);
continue;
}
messageDAO.create(new AccountMessage()


+ 9
- 2
bubble-server/src/main/java/bubble/service/cloud/NetworkMonitorService.java View File

@@ -10,8 +10,10 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import static bubble.ApiConstants.ROOT_NETWORK_UUID;
import static java.util.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static org.cobbzilla.util.daemon.ZillaRuntime.shortError;
import static org.cobbzilla.util.time.TimeUtil.formatDuration;
import static org.cobbzilla.wizard.server.RestServerBase.reportError;

@Service @Slf4j
@@ -19,6 +21,7 @@ public class NetworkMonitorService extends SimpleDaemon {

private static final long STARTUP_DELAY = MINUTES.toMillis(1);
private static final long CHECK_INTERVAL = MINUTES.toMillis(30);
private static final long NO_NODES_GRACE_PERIOD = HOURS.toMillis(1);

@Override protected long getStartupDelay() { return STARTUP_DELAY; }
@Override protected long getSleepTime() { return CHECK_INTERVAL; }
@@ -52,8 +55,12 @@ public class NetworkMonitorService extends SimpleDaemon {
}
} else {
if (network.getState() != BubbleNetworkState.stopped && network.getState() != BubbleNetworkState.error_stopping) {
reportError(getName() + ": network " + network.getNetworkDomain() + " does NOT have nodes running but state is " + network.getState()+", marking it 'error_stopping'");
networkDAO.update(network.setState(BubbleNetworkState.error_stopping));
if (network.getCtimeAge() < NO_NODES_GRACE_PERIOD) {
log.warn(getName() + ": network " + network.getNetworkDomain() + " does NOT have nodes running but state is " + network.getState() + ", we would normally mark it 'error_stopping' but it is less than "+formatDuration(NO_NODES_GRACE_PERIOD)+" old");
} else {
reportError(getName() + ": network " + network.getNetworkDomain() + " does NOT have nodes running but state is " + network.getState() + ", marking it 'error_stopping'");
networkDAO.update(network.setState(BubbleNetworkState.error_stopping));
}
}
}
}


+ 1
- 1
bubble-server/src/main/resources/bubble-config.yml View File

@@ -61,7 +61,7 @@ jersey:

redis:
key: '{{#exists BUBBLE_REDIS_ENCRYPTION_KEY}}{{BUBBLE_REDIS_ENCRYPTION_KEY}}{{else}}{{key_file '.BUBBLE_REDIS_ENCRYPTION_KEY'}}{{/exists}}'
prefix: bubble_
prefix: bubble

errorApi:
url: {{ERRBIT_URL}}


+ 1
- 0
bubble-server/src/test/java/bubble/test/BubbleApiRunnerListener.java View File

@@ -52,6 +52,7 @@ public class BubbleApiRunnerListener extends SimpleApiRunnerListener {
final List<String> parts = splitAndTrim(before.substring(FAST_FORWARD_AND_BILL.length()), " ");
final long delta = parseDuration(parts.get(0));
final long sleepTime = parts.size() > 1 ? parseDuration(parts.get(1)) : DEFAULT_BILLING_SLEEP;
configuration.autowire(new StripePaymentDriver()).flushCaches();
incrementSystemTimeOffset(delta);
configuration.getBean(BillingService.class).processBilling();
sleep(sleepTime, "waiting for BillingService to complete");


+ 232
- 0
bubble-server/src/test/resources/models/tests/promo/first_month_free.json View File

@@ -98,6 +98,26 @@

{
"before": "sleep 15s",
"comment": "start the network",
"request": {
"uri": "me/networks/{{accountPlan.network}}/actions/start?cloud=MockCompute&region=nyc_mock",
"method": "post"
},
"response": {
"store": "newNetworkNotification"
}
},

{
"before": "sleep 5s",
"comment": "verify the network is running",
"request": { "uri": "me/networks/{{accountPlan.network}}" },
"response": {
"check": [ {"condition": "json.getState().name() == 'running'"} ]
}
},

{
"comment": "list all account payment methods, should be two",
"request": { "uri": "me/paymentMethods?all=true" },
"response": {
@@ -203,5 +223,217 @@
{"condition": "json[0].getBillObject().getStatus().name() === 'paid'"}
]
}
},

{
"comment": "add second plan, using same payment method, does NOT apply 1mo free promo because it's already been used",
"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",
"paymentMethodObject": {
"uuid": "{{find paymentMethods 'paymentMethodType' 'credit' 'uuid'}}"
}
}
},
"response": {
"store": "accountPlan2"
}
},

{
"before": "sleep 15s",
"comment": "start the 2nd network",
"request": {
"uri": "me/networks/{{accountPlan2.network}}/actions/start?cloud=MockCompute&region=nyc_mock",
"method": "post"
},
"response": {
"store": "newNetworkNotification"
}
},

{
"before": "sleep 5s",
"comment": "verify the 2nd network is running",
"request": { "uri": "me/networks/{{accountPlan2.network}}" },
"response": {
"check": [ {"condition": "json.getState().name() == 'running'"} ]
}
},

{
"comment": "list all account payment methods, should STILL be two",
"request": { "uri": "me/paymentMethods?all=true" },
"response": {
"store": "paymentMethods",
"check": [
{"condition": "json.length === 2"},
{"condition": "_find(json, function(p) { return p.getPaymentMethodType().name() === 'credit'; }) !== null"},
{"condition": "_find(json, function(p) { return p.getPaymentMethodType().name() === 'credit'; }).deleted() === false"},
{"condition": "_find(json, function(p) { return p.getPaymentMethodType().name() === 'promotional_credit'; }) !== null"},
{"condition": "_find(json, function(p) { return p.getPaymentMethodType().name() === 'promotional_credit'; }).deleted() === true"}
]
}
},

{
"comment": "verify account plans, should now be two",
"request": { "uri": "me/plans" },
"response": {
"check": [
{"condition": "json.length === 2"},
{"condition": "json[0].enabled()"},
{"condition": "json[1].enabled()"}
]
}
},

{
"comment": "verify 2nd account plan payment info",
"request": { "uri": "me/plans/{{accountPlan2.uuid}}/paymentMethod" },
"response": {
"check": [
{"condition": "json.getPaymentMethodType().name() === 'credit'"},
{"condition": "json.getMaskedPaymentInfo() == 'XXXX-XXXX-XXXX-4242'"}
]
}
},

{
"comment": "verify new bill exists for 2nd plan and was paid",
"request": { "uri": "me/bills" },
"response": {
"check": [
{"condition": "json.length === 2"},
{"condition": "json[0].getPlan() === '{{plans.[0].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}}"},
{"condition": "json[0].getStatus().name() === 'paid'"}
]
}
},

{
"comment": "verify bill exists for 2nd plan and is paid",
"request": { "uri": "me/plans/{{accountPlan2.uuid}}/bills" },
"response": {
"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() === {{plans.[0].price}}"},
{"condition": "json[0].getTotal() === {{plans.[0].price}}"},
{"condition": "json[0].getStatus().name() === 'paid'"}
]
}
},

{
"comment": "verify payment for 2nd plan exists and is successful via credit card",
"request": { "uri": "me/payments" },
"response": {
"check": [
{"condition": "json.length === 2"},
{"condition": "json[0].getPlan() === '{{plans.[0].uuid}}'"},
{"condition": "json[0].getAccountPlan() === '{{accountPlan2.uuid}}'"},
{"condition": "json[0].getAmount() === {{plans.[0].price}}"},
{"condition": "json[0].getStatus().name() === 'success'"},
{"condition": "json[0].getType().name() === 'payment'"},
{"condition": "json[0].getPaymentMethod() === _find(paymentMethods, function(p) { return p.getPaymentMethodType().name() === 'credit'; }).getUuid()"}
]
}
},

{
"comment": "verify payment for 2nd plan exists via plan and is successful via credit card",
"request": { "uri": "me/plans/{{accountPlan2.uuid}}/payments" },
"response": {
"check": [
{"condition": "json.length === 1"},
{"condition": "json[0].getPlan() === '{{plans.[0].uuid}}'"},
{"condition": "json[0].getAccountPlan() === '{{accountPlan2.uuid}}'"},
{"condition": "json[0].getAmount() === {{plans.[0].price}}"},
{"condition": "json[0].getStatus().name() === 'success'"},
{"condition": "json[0].getType().name() === 'payment'"},
{"condition": "json[0].getPaymentMethod() === _find(paymentMethods, function(p) { return p.getPaymentMethodType().name() === 'credit'; }).getUuid()"},
{"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() === {{plans.[0].price}}"},
{"condition": "json[0].getBillObject().getTotal() === {{plans.[0].price}}"},
{"condition": "json[0].getBillObject().getStatus().name() === 'paid'"}
]
}
},

{
"before": "fast_forward_and_bill 31d 20s",
"comment": "fast-forward +31 days, verify a new bill exists for first accountPlan",
"request": { "uri": "me/plans/{{accountPlan.uuid}}/bills" },
"response": {
"check": [
{"condition": "json.length === 2"},
{"condition": "json[0].getPlan() === '{{plans.[0].uuid}}'"},
{"condition": "json[0].getAccountPlan() === '{{accountPlan.uuid}}'"},
{"condition": "json[0].getQuantity() === 1"},
{"condition": "json[0].getPrice() === {{plans.[0].price}}"},
{"condition": "json[0].getTotal() === {{plans.[0].price}}"},
{"condition": "json[0].getStatus().name() === 'paid'"}
]
}
},

{
"comment": "Verify a successful payment for accountPlan has been made via credit card",
"request": { "uri": "me/plans/{{accountPlan.uuid}}/payments" },
"response": {
"check": [
{"condition": "json.length === 2"},
{"condition": "json[0].getPlan() === '{{plans.[0].uuid}}'"},
{"condition": "json[0].getAccountPlan() === '{{accountPlan.uuid}}'"},
{"condition": "json[0].getAmount() === {{plans.[0].price}}"},
{"condition": "json[0].getStatus().name() === 'success'"},
{"condition": "json[0].getType().name() === 'payment'"},
{"condition": "json[0].getPaymentMethod() === _find(paymentMethods, function(p) { return p.getPaymentMethodType().name() === 'credit'; }).getUuid()"},
{"condition": "json[0].getBillObject().getPlan() === '{{plans.[0].uuid}}'"},
{"condition": "json[0].getBillObject().getAccountPlan() === '{{accountPlan.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 a successful payment for accountPlan2 has been made via credit card",
"request": { "uri": "me/plans/{{accountPlan2.uuid}}/payments" },
"response": {
"check": [
{"condition": "json.length === 2"},
{"condition": "json[0].getPlan() === '{{plans.[0].uuid}}'"},
{"condition": "json[0].getAccountPlan() === '{{accountPlan2.uuid}}'"},
{"condition": "json[0].getAmount() === {{plans.[0].price}}"},
{"condition": "json[0].getStatus().name() === 'success'"},
{"condition": "json[0].getType().name() === 'payment'"},
{"condition": "json[0].getPaymentMethod() === _find(paymentMethods, function(p) { return p.getPaymentMethodType().name() === 'credit'; }).getUuid()"},
{"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() === {{plans.[0].price}}"},
{"condition": "json[0].getBillObject().getTotal() === {{plans.[0].price}}"},
{"condition": "json[0].getBillObject().getStatus().name() === 'paid'"}
]
}
}
]

+ 1
- 1
bubble-server/src/test/resources/test-bubble-config.yml View File

@@ -60,7 +60,7 @@ jersey:

redis:
key: '{{#exists BUBBLE_REDIS_ENCRYPTION_KEY}}{{BUBBLE_REDIS_ENCRYPTION_KEY}}{{else}}{{key_file '.BUBBLE_REDIS_ENCRYPTION_KEY'}}{{/exists}}'
prefix: bubble_
prefix: bubble

errorApi:
url: {{ERRBIT_URL}}


+ 1
- 1
utils/cobbzilla-wizard

@@ -1 +1 @@
Subproject commit 0262f774014d5d6cea6451c0ad68dd697f8b9625
Subproject commit f7108ec232850b7a6f1a0c81e93f16a44285b2dc

Loading…
Cancel
Save