@@ -30,8 +30,9 @@ public interface PromotionalPaymentServiceDriver extends PaymentServiceDriver { | |||
AccountPlan accountPlan, | |||
AccountPaymentMethod paymentMethod) { | |||
// do not use if deleted (should never happen) | |||
// do not use if wrong currency (should never happen) | |||
// do not use if other higher priority promotions are usable | |||
return paymentMethod.notDeleted() && usable.isEmpty(); | |||
return paymentMethod.notDeleted() && promo.isCurrency(bill.getCurrency()) && usable.isEmpty(); | |||
} | |||
} |
@@ -8,6 +8,9 @@ import org.hibernate.criterion.Order; | |||
import org.springframework.beans.factory.annotation.Autowired; | |||
import org.springframework.stereotype.Repository; | |||
import java.util.Set; | |||
import java.util.stream.Collectors; | |||
@Repository | |||
public class BubblePlanDAO extends AccountOwnedEntityDAO<BubblePlan> { | |||
@@ -38,4 +41,8 @@ public class BubblePlanDAO extends AccountOwnedEntityDAO<BubblePlan> { | |||
return findByName(id); | |||
} | |||
public Set<String> getSupportedCurrencies () { | |||
return findAll().stream().map(BubblePlan::getCurrency).collect(Collectors.toSet()); | |||
} | |||
} |
@@ -25,27 +25,47 @@ public class PromotionDAO extends AbstractCRUDDAO<Promotion> { | |||
return found != null ? found : findByName(id); | |||
} | |||
public Promotion findEnabledAndActiveWithCode(String code) { | |||
return filterActive(findByUniqueFields("enabled", true, "code", code, "referral", false, "adminAssignOnly", false)); | |||
public Promotion findEnabledAndActiveWithCode(String code, String currency) { | |||
return filterActive(findByUniqueFields( | |||
"enabled", true, | |||
"code", code, | |||
"referral", false, | |||
"currency", currency, | |||
"adminAssignOnly", false)); | |||
} | |||
public List<Promotion> findEnabledAndActiveWithNoCode() { | |||
return filterActive(findByFields("enabled", true, "code", null, "referral", false, "adminAssignOnly", false)); | |||
public List<Promotion> findEnabledAndActiveWithNoCode(String currency) { | |||
return filterActive(findByFields( | |||
"enabled", true, | |||
"code", null, | |||
"referral", false, | |||
"currency", currency, | |||
"adminAssignOnly", false)); | |||
} | |||
public List<Promotion> findVisibleAndEnabledAndActiveWithNoCodeOrWithCode(String code) { | |||
public List<Promotion> findVisibleAndEnabledAndActiveWithNoCodeOrWithCode(String code, String currency) { | |||
if (empty(code)) { | |||
return filterActive(findByFields("enabled", true, "code", null, "visible", true, "adminAssignOnly", false)); | |||
return filterActive(findByFields( | |||
"enabled", true, | |||
"code", null, | |||
"visible", true, | |||
"currency", currency, | |||
"adminAssignOnly", false)); | |||
} else { | |||
return filterActive(list(criteria().add(and( | |||
eq("enabled", true), | |||
eq("visible", true), | |||
eq("currency", currency), | |||
or(isNull("code"), eq("code", code)))))); | |||
} | |||
} | |||
public List<Promotion> findEnabledAndActiveWithReferral() { | |||
return filterActive(findByFields("enabled", true, "referral", true, "adminAssignOnly", false)); | |||
public List<Promotion> findEnabledAndActiveWithReferral(String currency) { | |||
return filterActive(findByFields( | |||
"enabled", true, | |||
"referral", true, | |||
"currency", currency, | |||
"adminAssignOnly", false)); | |||
} | |||
public Promotion filterActive(Promotion promo) { return promo != null && promo.active() ? promo : null; } | |||
@@ -98,6 +98,7 @@ public class AccountPayment extends IdentifiableBase implements HasAccountNoName | |||
} | |||
@Transient @Getter @Setter private transient Bill billObject; | |||
@Transient @Getter @Setter private transient AccountPaymentMethod paymentMethodObject; | |||
public static int totalPayments (List<AccountPayment> payments) { | |||
return empty(payments) ? 0 : payments.stream().mapToInt(AccountPayment::getAmountInt).sum(); | |||
@@ -12,6 +12,7 @@ import org.cobbzilla.wizard.model.entityconfig.annotations.*; | |||
import org.hibernate.annotations.Type; | |||
import javax.persistence.*; | |||
import java.util.List; | |||
import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.ENCRYPTED_LONG; | |||
import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.ENC_LONG; | |||
@@ -74,5 +75,6 @@ public class Bill extends IdentifiableBase implements HasAccountNoName { | |||
public boolean hasRefundedAmount () { return refundedAmount != null && refundedAmount > 0L; } | |||
@Transient @Getter @Setter private transient BubblePlan planObject; | |||
@Transient @Getter @Setter private transient List<AccountPayment> payments; | |||
} |
@@ -114,6 +114,10 @@ public class Promotion extends IdentifiableBase | |||
@ECIndex @Column(nullable=false, updatable=false, length=10) | |||
@Getter @Setter private String currency; | |||
public boolean isCurrency(String currency) { | |||
return currency != null && currency.equalsIgnoreCase(this.currency); | |||
} | |||
@ECSearchable @ECField(index=120) | |||
@ECIndex @Column(nullable=false, updatable=false) | |||
@Getter @Setter private Integer minValue = 100; | |||
@@ -4,6 +4,7 @@ import bubble.dao.SessionDAO; | |||
import bubble.dao.account.AccountDAO; | |||
import bubble.dao.account.AccountPolicyDAO; | |||
import bubble.dao.account.message.AccountMessageDAO; | |||
import bubble.dao.bill.BubblePlanDAO; | |||
import bubble.dao.cloud.BubbleNodeDAO; | |||
import bubble.model.CertType; | |||
import bubble.model.account.*; | |||
@@ -47,11 +48,13 @@ import static bubble.model.account.Account.validatePassword; | |||
import static bubble.model.cloud.BubbleNetwork.TAG_ALLOW_REGISTRATION; | |||
import static bubble.model.cloud.BubbleNetwork.TAG_PARENT_ACCOUNT; | |||
import static bubble.model.cloud.notify.NotificationType.retrieve_backup; | |||
import static bubble.server.BubbleConfiguration.getDEFAULT_LOCALE; | |||
import static bubble.server.BubbleServer.getRestoreKey; | |||
import static java.util.concurrent.TimeUnit.SECONDS; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.*; | |||
import static org.cobbzilla.util.http.HttpContentTypes.APPLICATION_JSON; | |||
import static org.cobbzilla.util.http.HttpContentTypes.CONTENT_TYPE_ANY; | |||
import static org.cobbzilla.util.string.LocaleUtil.currencyForLocale; | |||
import static org.cobbzilla.util.system.Sleep.sleep; | |||
import static org.cobbzilla.wizard.resources.ResourceUtil.*; | |||
@@ -70,6 +73,7 @@ public class AuthResource { | |||
@Autowired private ActivationService activationService; | |||
@Autowired private AccountMessageDAO accountMessageDAO; | |||
@Autowired private StandardAccountMessageService messageService; | |||
@Autowired private BubblePlanDAO planDAO; | |||
@Autowired private BubbleNodeDAO nodeDAO; | |||
@Autowired private BubbleConfiguration configuration; | |||
@Autowired private AuthenticatorService authenticatorService; | |||
@@ -197,8 +201,14 @@ public class AuthResource { | |||
request.getContact().validate(errors); | |||
} | |||
String currency = null; | |||
if (configuration.paymentsEnabled()) { | |||
errors.addAll(promoService.validatePromotions(request.getPromoCode())); | |||
currency = currencyForLocale(request.getLocale(), getDEFAULT_LOCALE()); | |||
// do we have any plans with this currency? | |||
if (!planDAO.getSupportedCurrencies().contains(currency)) { | |||
currency = currencyForLocale(getDEFAULT_LOCALE()); | |||
} | |||
errors.addAll(promoService.validatePromotions(request.getPromoCode(), currency)); | |||
} | |||
if (errors.isInvalid()) return invalid(errors); | |||
@@ -211,7 +221,7 @@ public class AuthResource { | |||
SimpleViolationException promoEx = null; | |||
if (configuration.paymentsEnabled()) { | |||
try { | |||
promoService.applyPromotions(account, request.getPromoCode()); | |||
promoService.applyPromotions(account, request.getPromoCode(), currency); | |||
} catch (SimpleViolationException e) { | |||
promoEx = e; | |||
} | |||
@@ -6,14 +6,16 @@ import bubble.dao.account.AccountSshKeyDAO; | |||
import bubble.dao.bill.AccountPaymentMethodDAO; | |||
import bubble.dao.bill.AccountPlanDAO; | |||
import bubble.dao.bill.BubblePlanDAO; | |||
import bubble.dao.bill.PromotionDAO; | |||
import bubble.dao.cloud.BubbleDomainDAO; | |||
import bubble.dao.cloud.BubbleFootprintDAO; | |||
import bubble.dao.cloud.BubbleNetworkDAO; | |||
import bubble.dao.cloud.CloudServiceDAO; | |||
import bubble.model.account.Account; | |||
import bubble.model.account.AccountSshKey; | |||
import bubble.model.bill.*; | |||
import bubble.model.bill.AccountPaymentMethod; | |||
import bubble.model.bill.AccountPlan; | |||
import bubble.model.bill.BubblePlan; | |||
import bubble.model.bill.PaymentMethodType; | |||
import bubble.model.cloud.BubbleDomain; | |||
import bubble.model.cloud.BubbleFootprint; | |||
import bubble.model.cloud.BubbleNetwork; | |||
@@ -56,7 +58,6 @@ public class AccountPlansResource extends AccountOwnedResource<AccountPlan, Acco | |||
@Autowired private BubbleConfiguration configuration; | |||
@Autowired private AuthenticatorService authenticatorService; | |||
@Autowired private GeoService geoService; | |||
@Autowired private PromotionDAO promotionDAO; | |||
public AccountPlansResource(Account account) { super(account); } | |||
@@ -1,16 +1,10 @@ | |||
package bubble.resources.bill; | |||
import bubble.cloud.payment.PaymentServiceDriver; | |||
import bubble.dao.bill.AccountPaymentMethodDAO; | |||
import bubble.dao.bill.AccountPlanDAO; | |||
import bubble.dao.bill.BillDAO; | |||
import bubble.dao.bill.BubblePlanDAO; | |||
import bubble.dao.bill.*; | |||
import bubble.dao.cloud.CloudServiceDAO; | |||
import bubble.model.account.Account; | |||
import bubble.model.bill.AccountPaymentMethod; | |||
import bubble.model.bill.AccountPlan; | |||
import bubble.model.bill.Bill; | |||
import bubble.model.bill.BubblePlan; | |||
import bubble.model.bill.*; | |||
import bubble.model.cloud.CloudService; | |||
import bubble.resources.account.ReadOnlyAccountOwnedResource; | |||
import lombok.extern.slf4j.Slf4j; | |||
@@ -30,14 +24,18 @@ import java.util.Map; | |||
import static bubble.ApiConstants.EP_PAY; | |||
import static bubble.ApiConstants.EP_PAYMENTS; | |||
import static org.cobbzilla.util.http.URIUtil.queryParams; | |||
import static org.cobbzilla.wizard.resources.ResourceUtil.*; | |||
@Slf4j | |||
public class BillsResource extends ReadOnlyAccountOwnedResource<Bill, BillDAO> { | |||
public static final String PARAM_PAYMENTS = "payments"; | |||
@Autowired private BubblePlanDAO planDAO; | |||
@Autowired private AccountPlanDAO accountPlanDAO; | |||
@Autowired private AccountPaymentMethodDAO paymentMethodDAO; | |||
@Autowired private AccountPaymentDAO paymentDAO; | |||
@Autowired private CloudServiceDAO cloudDAO; | |||
private AccountPlan accountPlan; | |||
@@ -51,7 +49,24 @@ public class BillsResource extends ReadOnlyAccountOwnedResource<Bill, BillDAO> { | |||
@Override protected Bill find(ContainerRequest ctx, String id) { | |||
final Bill bill = super.find(ctx, id); | |||
return bill == null || (accountPlan != null && !bill.getAccountPlan().equals(accountPlan.getUuid())) ? null : bill; | |||
if (bill == null || (accountPlan != null && !bill.getAccountPlan().equals(accountPlan.getUuid()))) return null; | |||
final Map<String, String> params = queryParams(ctx.getRequestUri().getQuery()); | |||
if (Boolean.parseBoolean(params.get(PARAM_PAYMENTS))) { | |||
final List<AccountPayment> payments = paymentDAO.findByAccountAndAccountPlanAndBill(bill.getAccount(), bill.getAccountPlan(), bill.getUuid()); | |||
for (AccountPayment payment : payments) { | |||
final String paymentMethodUuid = payment.getPaymentMethod(); | |||
payment.setPaymentMethodObject(findPaymentMethod(paymentMethodUuid)); | |||
} | |||
return bill.setPayments(payments); | |||
} | |||
return bill; | |||
} | |||
private Map<String, AccountPaymentMethod> paymentMethodCache = new ExpirationMap<>(ExpirationEvictionPolicy.atime); | |||
private AccountPaymentMethod findPaymentMethod(String paymentMethodUuid) { | |||
return paymentMethodCache.computeIfAbsent(paymentMethodUuid, k -> paymentMethodDAO.findByUuid(k)); | |||
} | |||
@Override protected List<Bill> list(ContainerRequest ctx) { | |||
@@ -64,7 +79,7 @@ public class BillsResource extends ReadOnlyAccountOwnedResource<Bill, BillDAO> { | |||
} | |||
private Map<String, BubblePlan> planCache = new ExpirationMap<>(ExpirationEvictionPolicy.atime); | |||
private BubblePlan findPlan(String planUuid) { return planCache.computeIfAbsent(planUuid, k -> planDAO.findByUuid(planUuid)); } | |||
private BubblePlan findPlan(String planUuid) { return planCache.computeIfAbsent(planUuid, k -> planDAO.findByUuid(k)); } | |||
@Path("/{id}"+EP_PAYMENTS) | |||
public AccountPaymentsResource getPayments(@Context ContainerRequest ctx, | |||
@@ -18,7 +18,10 @@ import javax.ws.rs.core.Context; | |||
import javax.ws.rs.core.Response; | |||
import static bubble.ApiConstants.PROMOTIONS_ENDPOINT; | |||
import static bubble.server.BubbleConfiguration.getDEFAULT_LOCALE; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.empty; | |||
import static org.cobbzilla.util.http.HttpContentTypes.APPLICATION_JSON; | |||
import static org.cobbzilla.util.string.LocaleUtil.currencyForLocale; | |||
import static org.cobbzilla.wizard.resources.ResourceUtil.*; | |||
@Consumes(APPLICATION_JSON) | |||
@@ -35,12 +38,14 @@ public class PromotionsResource { | |||
@GET | |||
public Response listPromos(@Context ContainerRequest ctx, | |||
@QueryParam("currency") String currency, | |||
@QueryParam("code") String code) { | |||
if (empty(currency)) currency = currencyForLocale(getDEFAULT_LOCALE()); | |||
final Account caller = optionalUserPrincipal(ctx); | |||
if (caller != null && caller.admin()) { | |||
return ok(promotionDAO.findAll()); | |||
} | |||
return ok(promotionDAO.findVisibleAndEnabledAndActiveWithNoCodeOrWithCode(code)); | |||
return ok(promotionDAO.findVisibleAndEnabledAndActiveWithNoCodeOrWithCode(code, currency)); | |||
} | |||
@GET @Path("/{id}") | |||
@@ -42,19 +42,19 @@ public class PromotionService { | |||
@Autowired protected AccountPaymentMethodDAO accountPaymentMethodDAO; | |||
@Autowired private BubbleConfiguration configuration; | |||
public void applyPromotions(Account account, String code) { | |||
public void applyPromotions(Account account, String code, String currency) { | |||
// apply promo code (or default) promotion | |||
final Set<Promotion> promos = new TreeSet<>(); | |||
ReferralCode referralCode = null; | |||
if (!empty(code)) { | |||
Promotion promo = promotionDAO.findEnabledAndActiveWithCode(code); | |||
Promotion promo = promotionDAO.findEnabledAndActiveWithCode(code, currency); | |||
if (promo == null) { | |||
// check referral codes | |||
// it might be a referral code | |||
referralCode = referralCodeDAO.findByName(code); | |||
if (referralCode != null && !referralCode.claimed()) { | |||
// is there a referral promotion we can use? | |||
for (Promotion p : promotionDAO.findEnabledAndActiveWithReferral()) { | |||
for (Promotion p : promotionDAO.findEnabledAndActiveWithReferral(currency)) { | |||
promos.add(p); | |||
break; | |||
} | |||
@@ -66,7 +66,7 @@ public class PromotionService { | |||
} | |||
// everyone gets the highest-priority default promotion, if there are any enabled and active | |||
for (Promotion p : promotionDAO.findEnabledAndActiveWithNoCode()) { | |||
for (Promotion p : promotionDAO.findEnabledAndActiveWithNoCode(currency)) { | |||
promos.add(p); | |||
break; | |||
} | |||
@@ -105,9 +105,9 @@ public class PromotionService { | |||
} | |||
} | |||
public ValidationResult validatePromotions(String code) { | |||
public ValidationResult validatePromotions(String code, String currency) { | |||
if (!empty(code)) { | |||
Promotion promo = promotionDAO.findEnabledAndActiveWithCode(code); | |||
Promotion promo = promotionDAO.findEnabledAndActiveWithCode(code, currency); | |||
if (promo == null) { | |||
// it might be a referral code | |||
final ReferralCode referralCode = referralCodeDAO.findByName(code); | |||
@@ -116,7 +116,7 @@ public class PromotionService { | |||
if (referer == null || referer.deleted()) return new ValidationResult("err.promoCode.notFound"); | |||
// is there a referral promotion we can use? | |||
for (Promotion p : promotionDAO.findEnabledAndActiveWithReferral()) { | |||
for (Promotion p : promotionDAO.findEnabledAndActiveWithReferral(currency)) { | |||
// todo: add JS check? | |||
promo = p; | |||
break; | |||
@@ -167,11 +167,15 @@ public class PromotionService { | |||
final CloudService promoCloud = cloudDAO.findByUuid(promo.getCloud()); | |||
final String prefix = getClass().getSimpleName()+": "; | |||
if (promoCloud == null) { | |||
reportError(prefix+"purchase: cloud "+promo.getCloud()+" not found for promotion "+promo.getUuid()); | |||
reportError(prefix+"purchase: cloud "+promo.getCloud()+" not found for promotion "+promo.getName()); | |||
continue; | |||
} | |||
if (promoCloud.getType() != CloudServiceType.payment) { | |||
reportError(prefix+"purchase: cloud "+promo.getCloud()+" for promotion "+promo.getUuid()+" has wrong type (expected 'payment'): "+promoCloud.getType()); | |||
reportError(prefix+"purchase: cloud "+promo.getCloud()+" for promotion "+promo.getName()+" has wrong type (expected 'payment'): "+promoCloud.getType()); | |||
continue; | |||
} | |||
if (!promo.getCurrency().equals(plan.getCurrency())) { | |||
reportError(prefix+"purchase: promotion "+promo.getName()+" has wrong currency (expected "+plan.getCurrency()+" for plan "+plan.getName()+"): "+promoCloud.getType()); | |||
continue; | |||
} | |||
log.info("purchase: using Promotion: "+promo.getName()); | |||
@@ -242,10 +242,48 @@ label_bill_status=Status | |||
bill_status_paid=Paid | |||
bill_status_unpaid=Unpaid | |||
bill_status_partial_payment=Partial payment | |||
label_bill_period=Period | |||
bill_status_undefined=Unknown | |||
bill_status_null=Unknown | |||
bill_status_=Unknown | |||
label_bill_period=Date | |||
label_bill_period_start=From | |||
label_bill_period_end=To | |||
label_bill_total=Amount | |||
label_bill_total_format={{messages['currency_symbol_'+currency.toUpperCase()]}}{{totalMajorUnits}}{{totalMinorUnits === 0 ? '' : totalMinorUnits < 10 ? '.0'+totalMinorUnits : '.'+totalMinorUnits}} | |||
label_bill_refunded=refunded | |||
label_payment_type=Type | |||
label_payment_method=Paid By | |||
label_payment_status=Status | |||
label_payment_amount=Amount | |||
label_payment_amount_format={{messages['currency_symbol_'+currency.toUpperCase()]}}{{amountMajorUnits}}{{amountMinorUnits === 0 ? '' : amountMinorUnits < 10 ? '.0'+amountMinorUnits : '.'+amountMinorUnits}} | |||
label_payment_action=Action | |||
button_label_close_bill_detail=Close | |||
payment_method_credit=Credit/Debit Card | |||
payment_method_code=Invitation Code | |||
payment_method_free=Free! | |||
payment_method_promotional_credit=Promotion | |||
payment_method_undefined= | |||
payment_method_null= | |||
payment_method_= | |||
payment_status_init=Created | |||
payment_status_success=Success | |||
payment_status_failure=Failure | |||
payment_status_unknown=Unknown | |||
payment_status_undefined=Unknown | |||
payment_status_null=Unknown | |||
payment_status_=Unknown | |||
payment_type_payment=payment | |||
payment_type_credit_applied=credit applied | |||
payment_type_refund=refund | |||
label_promotion_FirstMonthFree=First Month Free | |||
label_promotion_ReferralMonthFree=Referral Bonus | |||
label_promotion_AccountCredit1=$1 Bonus | |||
label_promotion_AccountCredit5=$5 Bonus | |||
label_promotion_AccountCreditBill=Full Bill Bonus ($100 max value) | |||
# Bubble Plans | |||
plan_name_bubble=Bubble Standard | |||
@@ -1 +1 @@ | |||
Subproject commit c194f2c575af7d7735759759ee3dbfda5c768a15 | |||
Subproject commit ddbe7e69b39138c458766c82ee616c31fa7980d1 |
@@ -1 +1 @@ | |||
Subproject commit d1d485b1a8dcd51da565ca21886a95a728f3a832 | |||
Subproject commit 77831c8f23574ebdc8476dd835f8bbfbd8404338 |