Commit 1a1dec84 by Tuukka Kivilahti

Merge branch 'unpaidbillplaceslot' into 'master'

Limit number of places an unpaid bill can reserve

Limit the number of placeslots are counted when checking how many places an unpaid bill can reserve. This is done to limit the possibility for abuse by creating a bill with large number of places without any intent for paying it

See merge request !245
2 parents 0ea792cf f186d2c3
......@@ -58,6 +58,7 @@ import fi.codecrew.moya.model.Discount;
import fi.codecrew.moya.model.EventUser;
import fi.codecrew.moya.model.FoodWave;
import fi.codecrew.moya.model.LanEvent;
import fi.codecrew.moya.model.LanEventPropertyKey;
import fi.codecrew.moya.model.PlaceSlot;
import fi.codecrew.moya.model.Product;
import fi.codecrew.moya.model.ProductFlag;
......@@ -305,6 +306,12 @@ public class BillBean implements BillBeanLocal {
}
int count = bl.getQuantity().intValue();
Date now = new Date();
long reserveLimit = eventbean.getPropertyLong(LanEventPropertyKey.RESERVE_UNPAID_SLOT_PERCENT);
if (count * 100 / prod.getPlaces().size() > reserveLimit) {
long limitCount = prod.getPlaces().size() * reserveLimit / 100;
logbean.sendMessage(MoyaEventType.BILL_INFO, bill.getUser(), "User created bill ", bill.getId(), " for product '", prod.getName(), "' for '", count, "' products, which is bigger than reservation limit. '", limitCount, "', (", reserveLimit, "%)");
}
for (int i = 0; i < count; ++i) {
PlaceSlot ps = new PlaceSlot();
ps.setBill(bill);
......
......@@ -214,7 +214,7 @@ public class EventBean implements EventBeanLocal {
LanEventProperty retProp = eventPropertyFacade.find(getCurrentEvent(), property);
long ret = 0;
if (retProp == null) {
ret = Long.parseLong(property.getDefaultvalue());
ret = property.getDefaultLong();
} else {
ret = retProp.getLongValue();
}
......@@ -242,7 +242,7 @@ public class EventBean implements EventBeanLocal {
if (retProp == null) {
String def = property.getDefaultvalue();
if(def != null && !def.trim().isEmpty() && !def.trim().equals("0"))
if(def != null && !def.trim().isEmpty() && def.trim().equals("1"))
ret = true;
} else {
......
......@@ -55,10 +55,13 @@ import fi.codecrew.moya.facade.PlaceSlotFacade;
import fi.codecrew.moya.facade.ProductFacade;
import fi.codecrew.moya.facade.UserFacade;
import fi.codecrew.moya.model.AccountEvent;
import fi.codecrew.moya.model.Bill;
import fi.codecrew.moya.model.Discount;
import fi.codecrew.moya.model.EventUser;
import fi.codecrew.moya.model.InventoryEvent;
import fi.codecrew.moya.model.LanEvent;
import fi.codecrew.moya.model.LanEventPropertyKey;
import fi.codecrew.moya.model.PlaceSlot;
import fi.codecrew.moya.model.Product;
import fi.codecrew.moya.model.ProductFlag;
import fi.codecrew.moya.model.ProductLimitation;
......@@ -207,26 +210,81 @@ public class ProductBean implements ProductBeanLocal {
HashMap<Integer, BigDecimal> ret = new HashMap<>();
for (Entry<Integer, BigDecimal> pc : prodCounts.entrySet())
{
Product prod = productFacade.find(pc.getKey());
BigDecimal lim = getPrivateProductLimit(prod, user, prodCounts, userroles);
ret.put(prod.getId(), lim);
// logger.info("Added product limit {} to {}", lim, prod);
if (!prod.getProductFlags().contains(ProductFlag.PREPAID_CREDIT) && !prod.getProductFlags().contains(ProductFlag.CREATE_NEW_PLACE_WHEN_BOUGHT))
{
if (prod.getPlaces() != null && !prod.getPlaces().isEmpty()) {
// if this is a place product and not a prepaid credit product and places are not created on the fly
// get the available buyable places from database and check the number of unused places
if (!prod.getProductFlags().contains(ProductFlag.PREPAID_CREDIT)
&& !prod.getProductFlags().contains(ProductFlag.CREATE_NEW_PLACE_WHEN_BOUGHT)
&& prod.getPlaces() != null
&& !prod.getPlaces().isEmpty()) {
Long selectableCount = placeFacade.countSelectable(prod);
int unusedSlotcount = getUnusedSlotcount(prod);
int freeCount = selectableCount.intValue() - unusedSlotcount;
// logger.info("Prodlimit selectable {}, unused {}, free {}, prod {}", selectableCount, unusedSlots, freeCount, prod);
ret.put(prod.getId(), BigDecimal.valueOf(freeCount));
} else {
BigDecimal lim = getPrivateProductLimit(prod, user, prodCounts, userroles);
ret.put(prod.getId(), lim);
// logger.info("Added product limit {} to {}", lim, prod);
}
}
return ret;
Long selectableCount = placeFacade.countSelectable(prod);
Long unusedSlots = slotfacade.findUnusedSlotsCount(prod, false);
int freeCount = selectableCount.intValue() - unusedSlots.intValue();
}
/**
* How many unused slots there are for the product.
*
* @param prod
* @return
*/
private int getUnusedSlotcount(Product prod)
{
// How many percent of places can be reserved from shop by one unpaid bill
long unpaidSlotpercent = eventbean.getPropertyLong(LanEventPropertyKey.RESERVE_UNPAID_SLOT_PERCENT);
int unusedSlotcount = 0;
// If unpaid slot percent is 0 or 100, calculate directly from db.
if (unpaidSlotpercent < 1 || unpaidSlotpercent > 99) {
unusedSlotcount = slotfacade.findUnusedSlotsCount(prod, false).intValue();
} else {
// Calculate only 'unpaidSlotpercent' of total places to be reserver from one unpaid bill.
// This is to try to reduce the possibility of abuse by creating a 1000 place bill
// and obviously never intending to pay them.
final List<PlaceSlot> unusedSlots = slotfacade.findUnusedSlots(prod, false);
final Map<Integer, Integer> unpaidBills = new HashMap<>();
int prodPlacecount = prod.getPlaces().size();
for (PlaceSlot s : unusedSlots) {
// If bill s paid (or there is no bill), calculate all slots
if (s.getBill() == null || s.getBill().isPaid()) {
++unusedSlotcount;
} else {
logger.info("Prodlimit selectable {}, unused {}, free {}, prod {}", selectableCount, unusedSlots, freeCount, prod);
ret.put(prod.getId(), BigDecimal.valueOf(freeCount));
// If bill is paid, calculate only 'unpaidSlotpercent'% of slots
Integer billcnt = unpaidBills.get(s.getBill().getId());
if (billcnt == null) {
billcnt = 0;
}
if (billcnt * 100 / prodPlacecount < unpaidSlotpercent) {
++unusedSlotcount;
}
unpaidBills.put(s.getBill().getId(), ++billcnt);
}
}
}
}
return ret;
return unusedSlotcount;
}
@Override
......@@ -420,7 +478,7 @@ public class ProductBean implements ProductBeanLocal {
public AccountEvent createAccountEvent(Product product, BigDecimal overriddenUnitPrice, BigDecimal quantity, EventUser user) {
user = eventUserFacade.reload(user);
AccountEvent ret = productPBean.createAccountEvent(product, overriddenUnitPrice , quantity, user, Calendar.getInstance(), null);
AccountEvent ret = productPBean.createAccountEvent(product, overriddenUnitPrice, quantity, user, Calendar.getInstance(), null);
cardTemplateBean.checkPrintedCard(user);
return ret;
}
......@@ -565,13 +623,12 @@ public class ProductBean implements ProductBeanLocal {
List<Product> allProducts = productFacade.findAll();
List<Product> ticketProducts = new ArrayList<>();
for(Product product : allProducts) {
if(product.getProductFlags().contains(ProductFlag.RESERVE_PLACE_WHEN_BOUGHT)) {
for (Product product : allProducts) {
if (product.getProductFlags().contains(ProductFlag.RESERVE_PLACE_WHEN_BOUGHT)) {
ticketProducts.add(product);
} else if(product.getPlaces() != null && product.getPlaces().size() > 0) {
} else if (product.getPlaces() != null && product.getPlaces().size() > 0) {
ticketProducts.add(product);
}
......@@ -579,7 +636,6 @@ public class ProductBean implements ProductBeanLocal {
return ticketProducts;
// TODO: fix this facade and use it
//return placeFacade.getPlaceProducts();
......
......@@ -113,11 +113,22 @@ public class PlaceSlotFacade extends IntegerPkGenericFacade<PlaceSlot> {
return getEm().createQuery(q).getResultList();
}
public Long findUnusedSlotsCount(Product prod, boolean paidOnly) {
public List<PlaceSlot> findUnusedSlots(Product prod, boolean paidOnly) {
CriteriaBuilder cb = getEm().getCriteriaBuilder();
CriteriaQuery<Long> q = cb.createQuery(Long.class);
CriteriaQuery<PlaceSlot> q = cb.createQuery(PlaceSlot.class);
Root<PlaceSlot> root = q.from(PlaceSlot.class);
q.select(cb.count(root));
q.select(root);
List<Predicate> preds = getUnusedSlotPredicates(cb, root, prod, paidOnly);
q.where(preds.toArray(new Predicate[preds.size()]));
return getEm().createQuery(q).getResultList();
}
private static List<Predicate> getUnusedSlotPredicates(CriteriaBuilder cb, Root<PlaceSlot> root, Product prod, boolean paidOnly)
{
Path<Bill> bill = root.get(PlaceSlot_.bill);
List<Predicate> preds = new ArrayList<>();
......@@ -138,9 +149,18 @@ public class PlaceSlotFacade extends IntegerPkGenericFacade<PlaceSlot> {
// Check that slot is not used
preds.add(cb.isNull(root.get(PlaceSlot_.place)));
preds.add(cb.isNull(root.get(PlaceSlot_.used)));
return preds;
q.where(preds.toArray(new Predicate[preds.size()]));
}
public Long findUnusedSlotsCount(Product prod, boolean paidOnly) {
CriteriaBuilder cb = getEm().getCriteriaBuilder();
CriteriaQuery<Long> q = cb.createQuery(Long.class);
Root<PlaceSlot> root = q.from(PlaceSlot.class);
q.select(cb.count(root));
List<Predicate> preds = getUnusedSlotPredicates(cb, root, prod, paidOnly);
q.where(preds.toArray(new Predicate[preds.size()]));
Long count = super.getSingleNullableResult(getEm().createQuery(q));
return count;
......
......@@ -19,7 +19,7 @@
package fi.codecrew.moya.model;
public enum LanEventPropertyKey {
EVENT_LOGO(Type.DATA, null),
EVENT_LOGO(Type.DATA),
INVITEMAIL_SUBJECT(Type.TEXT, "Invitation to Moya Online Youth Accumulator"),
INVITEMAIL_CONTENT(Type.TEXT, "You have been invited to an event by {1}.\n\nYou can register to intranet at: {0}."),
BILL_PAID_MAIL_SUBJECT(Type.TEXT, "[{0}] Lasku merkitty maksetuksi"),
......@@ -29,25 +29,34 @@ public enum LanEventPropertyKey {
ADMIN_MAIL(Type.TEXT, "moya@codecrew.fi"),
EVENT_LAYOUT(Type.TEXT, "primelayout"),
SHOP_DEFAULT_CASH(Type.BOOL, null),
SHOP_DEFAULT_CASH(Type.BOOL),
PLACECODE_FROM_USER(Type.BOOL, "1"),
PLACECODE_PRINT_ONLY_OWN(Type.BOOL, null),
CHECK_BILL_STATS_PERMISSION(Type.BOOL, null),
GATHER_OTHER_BILL_INFO(Type.BOOL, null),
GATHER_SHIRT_SIZE(Type.BOOL, null),
ALLOW_BILLING(Type.BOOL, null),
BILL_EXPIRE_HOURS(Type.LONG, "1"),
TEMPLATE_PROPERTY1(Type.TEXT, null),
TEMPLATE_PROPERTY2(Type.TEXT, null),
TEMPLATE_PROPERTY3(Type.TEXT, null),
TEMPLATE_PROPERTY4(Type.TEXT, null),
TEMPLATE_PROPERTY5(Type.TEXT, null),
INVITE_ONLY_EVENT(Type.BOOL, null),
PLACECODE_PRINT_ONLY_OWN(Type.BOOL),
CHECK_BILL_STATS_PERMISSION(Type.BOOL),
GATHER_OTHER_BILL_INFO(Type.BOOL),
GATHER_SHIRT_SIZE(Type.BOOL),
ALLOW_BILLING(Type.BOOL),
BILL_EXPIRE_HOURS(Type.LONG, 1l),
TEMPLATE_PROPERTY1(Type.TEXT),
TEMPLATE_PROPERTY2(Type.TEXT),
TEMPLATE_PROPERTY3(Type.TEXT),
TEMPLATE_PROPERTY4(Type.TEXT),
TEMPLATE_PROPERTY5(Type.TEXT),
INVITE_ONLY_EVENT(Type.BOOL),
USE_ETICKET(Type.BOOL, "1"),
ETICKETMAIL_SUBJECT(Type.TEXT, "Your etickets to Moya Online Youth Accumulator"),
ETICKETMAIL_CONTENT(Type.TEXT, "Hello {1},\n\nYou can find your etickets to an event from: {0}"),
MAP_QUEUE(Type.BOOL, null),
DISABLE_PHOTO_ON_KIOSK(Type.BOOL, null),
/**
* Should users be put to queue when reserving places from map, ie. only a
* limited number of people can reserve places at once.
*/
MAP_QUEUE(Type.BOOL),
DISABLE_PHOTO_ON_KIOSK(Type.BOOL),
/**
* How many percent of the available places can be locked by one unpaid and
* unexpired bill
*/
RESERVE_UNPAID_SLOT_PERCENT(Type.LONG, 10l),
;
......@@ -57,6 +66,7 @@ public enum LanEventPropertyKey {
private final String defaultvalue;
private final Type type;
private final long defaultLong;
public boolean isText() {
return Type.TEXT.equals(type);
......@@ -78,10 +88,25 @@ public enum LanEventPropertyKey {
return Type.LONG.equals(type);
}
private LanEventPropertyKey(Type t)
{
this.type = t;
defaultvalue = null;
defaultLong = 0;
}
private LanEventPropertyKey(Type t, String def)
{
this.type = t;
defaultvalue = def;
defaultLong = 0;
}
private LanEventPropertyKey(Type t, Long def)
{
this.type = t;
defaultvalue = null;
defaultLong = def;
}
public String getDefaultvalue() {
......@@ -91,4 +116,8 @@ public enum LanEventPropertyKey {
public Type getType() {
return type;
}
public Long getDefaultLong() {
return defaultLong;
}
}
......@@ -15,6 +15,7 @@ public enum MoyaEventType {
USER_PERMISSION_VIOLATION(MoyaEventSource.USER),
LOGIN_SUCCESSFULL(MoyaEventSource.USER),
PLACE_ACTION(MoyaEventSource.PLACEMAP),
BILL_INFO(MoyaEventSource.SHOP),
;
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!