diff --git a/bubble-server/src/main/java/bubble/cloud/payment/promo/PromotionalPaymentServiceDriver.java b/bubble-server/src/main/java/bubble/cloud/payment/promo/PromotionalPaymentServiceDriver.java index f66e8ef6..25110440 100644 --- a/bubble-server/src/main/java/bubble/cloud/payment/promo/PromotionalPaymentServiceDriver.java +++ b/bubble-server/src/main/java/bubble/cloud/payment/promo/PromotionalPaymentServiceDriver.java @@ -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(); } } diff --git a/bubble-server/src/main/java/bubble/dao/bill/BubblePlanDAO.java b/bubble-server/src/main/java/bubble/dao/bill/BubblePlanDAO.java index 08cfee67..91c86ce3 100644 --- a/bubble-server/src/main/java/bubble/dao/bill/BubblePlanDAO.java +++ b/bubble-server/src/main/java/bubble/dao/bill/BubblePlanDAO.java @@ -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 { @@ -38,4 +41,8 @@ public class BubblePlanDAO extends AccountOwnedEntityDAO { return findByName(id); } + public Set getSupportedCurrencies () { + return findAll().stream().map(BubblePlan::getCurrency).collect(Collectors.toSet()); + } + } diff --git a/bubble-server/src/main/java/bubble/dao/bill/PromotionDAO.java b/bubble-server/src/main/java/bubble/dao/bill/PromotionDAO.java index 91bac3a2..1b920b52 100644 --- a/bubble-server/src/main/java/bubble/dao/bill/PromotionDAO.java +++ b/bubble-server/src/main/java/bubble/dao/bill/PromotionDAO.java @@ -25,27 +25,47 @@ public class PromotionDAO extends AbstractCRUDDAO { 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 findEnabledAndActiveWithNoCode() { - return filterActive(findByFields("enabled", true, "code", null, "referral", false, "adminAssignOnly", false)); + public List findEnabledAndActiveWithNoCode(String currency) { + return filterActive(findByFields( + "enabled", true, + "code", null, + "referral", false, + "currency", currency, + "adminAssignOnly", false)); } - public List findVisibleAndEnabledAndActiveWithNoCodeOrWithCode(String code) { + public List 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 findEnabledAndActiveWithReferral() { - return filterActive(findByFields("enabled", true, "referral", true, "adminAssignOnly", false)); + public List 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; } diff --git a/bubble-server/src/main/java/bubble/model/bill/AccountPayment.java b/bubble-server/src/main/java/bubble/model/bill/AccountPayment.java index 32b8b3b0..a4fe7c43 100644 --- a/bubble-server/src/main/java/bubble/model/bill/AccountPayment.java +++ b/bubble-server/src/main/java/bubble/model/bill/AccountPayment.java @@ -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 payments) { return empty(payments) ? 0 : payments.stream().mapToInt(AccountPayment::getAmountInt).sum(); 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 ffac3185..47ced402 100644 --- a/bubble-server/src/main/java/bubble/model/bill/Bill.java +++ b/bubble-server/src/main/java/bubble/model/bill/Bill.java @@ -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 payments; } diff --git a/bubble-server/src/main/java/bubble/model/bill/Promotion.java b/bubble-server/src/main/java/bubble/model/bill/Promotion.java index 84f78591..5c76ce4f 100644 --- a/bubble-server/src/main/java/bubble/model/bill/Promotion.java +++ b/bubble-server/src/main/java/bubble/model/bill/Promotion.java @@ -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; diff --git a/bubble-server/src/main/java/bubble/resources/account/AuthResource.java b/bubble-server/src/main/java/bubble/resources/account/AuthResource.java index 80215879..b9f0f117 100644 --- a/bubble-server/src/main/java/bubble/resources/account/AuthResource.java +++ b/bubble-server/src/main/java/bubble/resources/account/AuthResource.java @@ -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; } 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 6992ceee..e35c586b 100644 --- a/bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java +++ b/bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java @@ -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 { + 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 { @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 params = queryParams(ctx.getRequestUri().getQuery()); + if (Boolean.parseBoolean(params.get(PARAM_PAYMENTS))) { + final List 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 paymentMethodCache = new ExpirationMap<>(ExpirationEvictionPolicy.atime); + private AccountPaymentMethod findPaymentMethod(String paymentMethodUuid) { + return paymentMethodCache.computeIfAbsent(paymentMethodUuid, k -> paymentMethodDAO.findByUuid(k)); } @Override protected List list(ContainerRequest ctx) { @@ -64,7 +79,7 @@ public class BillsResource extends ReadOnlyAccountOwnedResource { } private Map 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, diff --git a/bubble-server/src/main/java/bubble/resources/bill/PromotionsResource.java b/bubble-server/src/main/java/bubble/resources/bill/PromotionsResource.java index 4e01a75a..8559937b 100644 --- a/bubble-server/src/main/java/bubble/resources/bill/PromotionsResource.java +++ b/bubble-server/src/main/java/bubble/resources/bill/PromotionsResource.java @@ -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}") diff --git a/bubble-server/src/main/java/bubble/service/bill/PromotionService.java b/bubble-server/src/main/java/bubble/service/bill/PromotionService.java index 3eeb027f..55037071 100644 --- a/bubble-server/src/main/java/bubble/service/bill/PromotionService.java +++ b/bubble-server/src/main/java/bubble/service/bill/PromotionService.java @@ -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 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()); diff --git a/bubble-server/src/main/resources/message_templates/en_US/server/post_auth/ResourceMessages.properties b/bubble-server/src/main/resources/message_templates/en_US/server/post_auth/ResourceMessages.properties index c3ed0755..cfc6b2bf 100644 --- a/bubble-server/src/main/resources/message_templates/en_US/server/post_auth/ResourceMessages.properties +++ b/bubble-server/src/main/resources/message_templates/en_US/server/post_auth/ResourceMessages.properties @@ -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 diff --git a/bubble-web b/bubble-web index c194f2c5..ddbe7e69 160000 --- a/bubble-web +++ b/bubble-web @@ -1 +1 @@ -Subproject commit c194f2c575af7d7735759759ee3dbfda5c768a15 +Subproject commit ddbe7e69b39138c458766c82ee616c31fa7980d1 diff --git a/utils/cobbzilla-utils b/utils/cobbzilla-utils index d1d485b1..77831c8f 160000 --- a/utils/cobbzilla-utils +++ b/utils/cobbzilla-utils @@ -1 +1 @@ -Subproject commit d1d485b1a8dcd51da565ca21886a95a728f3a832 +Subproject commit 77831c8f23574ebdc8476dd835f8bbfbd8404338