Skip to content

Instantly share code, notes, and snippets.

@jwgmeligmeyling
Created February 22, 2018 10:11
Show Gist options
  • Save jwgmeligmeyling/b1e700b765600a67c77349fba9b12047 to your computer and use it in GitHub Desktop.
Save jwgmeligmeyling/b1e700b765600a67c77349fba9b12047 to your computer and use it in GitHub Desktop.
package com.pallasathenagroup.entities;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonProperty.Access;
import com.fasterxml.jackson.annotation.JsonView;
import com.pallasathenagroup.entities.hermes.OrderEntry;
import com.pallasathenagroup.entities.hermes.ShoppingBasketEntry;
import lombok.*;
import org.hibernate.annotations.Formula;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
/**
* @author Jan-Willem Gmelig Meyling
*/
@Getter
@Setter
@Entity
@Table(name = "discount_code")
@ToString(of = {"id"})
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class DiscountCode extends IdIdentifiable<Long> implements Comparable<DiscountCode> {
public static final Comparator<DiscountCode> BY_DISCOUNT_ASC = Comparator.comparing(DiscountCode::getDiscountPercentage)
.thenComparingLong(DiscountCode::getId);
public static final Comparator<DiscountCode> BY_DISCOUNT_DESC = BY_DISCOUNT_ASC.reversed();
@Id
@Column(name = "discount_code_id")
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@NotNull
@Column(name = "discount_percentage", nullable = false, scale = 2)
private BigDecimal discountPercentage;
@JsonIgnore
@OneToMany(mappedBy = "discountCode", fetch = FetchType.LAZY)
private List<OrderEntry> applications;
/**
* Read-only property that contains the number of applications of this discount code.
* This only counts the applications where the payment is positive (either paid or
* likely to be paid, at least not timed-out, cancelled or refunded). This query
* also keeps in mind cases where there was no payment because a 100% discount code
* was used, e.g. the paid price for the entry was 0.
*
* @see com.pallasathenagroup.entities.hermes.PaymentStatusInfo#isPositive()
*/
@Setter(AccessLevel.NONE)
@JsonView(Views.Internal.class)
@Formula("(SELECT COALESCE(SUM(oe.quantity), 0) FROM order_entry oe " +
"LEFT JOIN paynl_transaction tx ON tx.order_id = oe.order_id " +
"WHERE (tx.state > 0 OR oe.paid_price = 0) AND oe.discount_code_id = discount_code_id)")
@JsonProperty(value = "numberOfApplications", access = Access.READ_ONLY)
private int numberOfApplications;
@JsonIgnore
public abstract int getRemainingApplications();
@JsonIgnore
public abstract LocalDateTime getExpires();
@JsonIgnore
public abstract Set<Scope<?, ?>> getApplicableScopes();
@Deprecated
public boolean applicableFor(ShoppingBasketEntry shoppingBasketEntry) {
return applicableFor(shoppingBasketEntry.getProduct());
}
public boolean applicableFor(Product<?,?> product) {
return product.getScope().traverseToParents()
.anyMatch(getApplicableScopes()::contains);
}
@JsonIgnore
public boolean hasExpired() {
return getExpires().isBefore(LocalDateTime.now());
}
@JsonIgnore
public boolean isActive() {
return !hasExpired();
}
@Override
public int compareTo(DiscountCode o) {
return BY_DISCOUNT_ASC.compare(this, o);
}
@JsonIgnore
public boolean notExpired() {
return !hasExpired();
}
public boolean hasHigherDiscountPercentageThan(DiscountCode discountCode) {
return getDiscountPercentage().compareTo(discountCode.getDiscountPercentage()) >= 0;
}
@JsonIgnore
public abstract String getDescription();
}
package com.pallasathenagroup.entities;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonView;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.pallasathenagroup.entities.hermes.Order;
import com.pallasathenagroup.entities.hermes.OrderEntry;
import com.pallasathenagroup.entities.hermes.ShoppingBasket;
import lombok.*;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;
import org.hibernate.annotations.Type;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.List;
import java.util.Set;
/**
* A {@code GlobalPartner} is a {@link Partner} that is connected to one or multiple {@link Scope Scopes}.
* These partners are usually student associations, that collect revenue for marketing efforts for a specific
* faculty or study track.
*
* The global partner system utilizes the allowedIps and the partner forward function.
*
* Global partners share a many-to-many relations to scope, because student associations are not
* study specific.
*
* @author Jan-Willem Gmelig Meyling
*/
@Getter
@Setter
@Entity
@JsonDeserialize
@Table(name = "global_partner")
@ToString(exclude = {"scopes", "orderEntries"}, callSuper = true)
public class GlobalPartner extends Partner {
@JsonIgnore
@JoinTable(name = "global_partner_scopes", joinColumns = {
@JoinColumn(name = "partner_id", referencedColumnName = "partner_id", nullable = false)
}, inverseJoinColumns = {
@JoinColumn(name = "scope_id", referencedColumnName = "scope_id", nullable = false)
})
@Fetch(FetchMode.SUBSELECT)
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private Set<Scope<?, ?>> scopes;
@Type(type = "StringJsonObject")
@JsonView(Views.Internal.class)
@Column(name="allowed_ips", columnDefinition = "json")
private Collection<String> allowedIps;
@NotNull
@JsonIgnore
@OneToOne(optional = false, cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "discount_code_id", referencedColumnName = "discount_code_id", nullable = false)
private PartnerDiscountCode discountCode;
@JsonIgnore
@OneToMany(mappedBy = "globalPartner", fetch = FetchType.LAZY)
private List<OrderEntry> orderEntries;
@JsonView(Views.Public.class)
public BigDecimal getDiscountPercentage() {
return getDiscountCode().getDiscountPercentage();
}
@JsonView(Views.Public.class)
public void setDiscountPercentage(BigDecimal value) {
if (discountCode == null) {
discountCode = new PartnerDiscountCode();
discountCode.setPartner(this);
}
discountCode.setDiscountPercentage(value);
}
@Override
public boolean isGlobal() {
return true;
}
}
package com.pallasathenagroup.entities;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonProperty.Access;
import com.fasterxml.jackson.annotation.JsonView;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.pallasathenagroup.entities.hermes.OrderEntry;
import com.pallasathenagroup.types.JsonType;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;
import org.hibernate.annotations.TypeDefs;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotBlank;
import javax.persistence.*;
import javax.validation.constraints.Pattern;
import java.io.IOException;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author Jan-Willem Gmelig Meyling
*/
@Getter
@Setter
@Entity
@Table(name = "partner")
@JsonIgnoreProperties(ignoreUnknown = true)
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
@JsonDeserialize(using = Partner.PartnerDeserializer.class)
@TypeDefs({@TypeDef( name= "StringJsonObject", typeClass = JsonType.class)})
public abstract class Partner extends IdIdentifiable<Long> {
@Id
@Column(name = "partner_id")
@JsonView(Views.Public.class)
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
@Column(name = "expires")
@JsonView(Views.Internal.class)
private LocalDateTime expires;
@JsonView(Views.Public.class)
@Column(name = "name", unique = true)
private String name;
@JsonView(Views.Internal.class)
@Column(name = "revenue_percentage", scale = 2)
private BigDecimal revenuePercentage;
@NotBlank
@Length(min = 2, max = 40)
@Pattern(regexp = "^[a-zA-Z0-9\\-]{2,40}$",
message = "Only a-z A-Z 0-9 and the dash (-) are allowed characters in a slug.")
@JsonView(Views.Public.class)
@Column(name = "slug", unique = true)
private String slug;
@Type(type = "StringJsonObject")
@JsonView(Views.Public.class)
@Column(name="visuals", columnDefinition = "json")
private Map<String, String> visuals;
@JsonProperty(value = "global", access = Access.READ_ONLY)
public abstract boolean isGlobal();
public abstract Set<Scope<?,?>> getScopes();
public abstract void setScopes(Set<Scope<?,?>> scopes);
static class PartnerDeserializer extends StdDeserializer<Partner> {
PartnerDeserializer() {
super(Partner.class);
}
@Override
public Partner deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
ObjectMapper mapper = (ObjectMapper) p.getCodec();
ObjectNode root = mapper.readTree(p);
JsonNode discountPercentage = root.get("discountPercentage");
JsonNode typeNode = root.get("global");
// No discountPercentage field, or field value null? -> GlobalPartner
boolean global = typeNode == null && discountPercentage != null && !discountPercentage.isNull();
// Global field defined and true? -> Global partner
global |= typeNode != null && typeNode.booleanValue();
// Convert JSON string to map... why
JsonNode visuals = root.get("visuals");
if (visuals != null && visuals.isTextual()) {
root.set("visuals", mapper.readTree(visuals.asText()));
}
Class<? extends Partner> clasz = global ? GlobalPartner.class : ScopePartner.class;
JsonParser traverse = root.traverse();
traverse.setCodec(mapper);
return mapper.readValue(traverse, clasz);
}
}
@JsonIgnore
public abstract List<OrderEntry> getOrderEntries();
}
package com.pallasathenagroup.entities;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.Locale;
import java.util.ResourceBundle;
import java.util.Set;
/**
* @author Jan-Willem Gmelig Meyling
*/
@Getter
@Setter
@Entity
@Table(name = "partner_discount_code")
@ToString(of = {}, callSuper = true)
@Inheritance(strategy = InheritanceType.JOINED)
public class PartnerDiscountCode extends DiscountCode {
@OneToOne(mappedBy = "discountCode", optional = false)
private GlobalPartner partner;
@Override
public int getRemainingApplications() {
return Integer.MAX_VALUE;
}
@Override
public LocalDateTime getExpires() {
return getPartner().getExpires();
}
@Override
public Set<Scope<?, ?>> getApplicableScopes() {
return getPartner().getScopes();
}
@Override
public String getDescription() {
Locale locale = getPartner().getScopes().stream()
.map(Scope::getInheritedLocale)
.findFirst()
.orElse(Locale.ENGLISH);
ResourceBundle resourceBundle = ResourceBundle.getBundle("com.pallasathenagroup.i18n", locale);
return resourceBundle.getString("partner-discount") + " " + getPartner().getName();
}
}
package com.pallasathenagroup.entities;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.pallasathenagroup.entities.hermes.OrderEntry;
import lombok.*;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import java.util.List;
import java.util.Set;
/**
* A {@code ScopePartner} is a {@link Partner} that is connected to one or multiple {@link Scope} Scopes.
* These partners are usually study associations, that collect revenue for marketing efforts for a specific
* faculty or study track.
*
* Implementors must ensure that a parent {@link Scope} does not get bound to a {@code ScopePartner} when
* one of its {@link Scope#getSubScopes() subscopes} is already bound to a {@code ScopePartner}.
*
* @author Jan-Willem Gmelig Meyling
*/
@Getter
@Setter
@Entity
@JsonDeserialize
@Table(name = "scope_partner")
@ToString(exclude = {"scopes", "orderEntries"}, callSuper = true)
public class ScopePartner extends Partner {
@JsonIgnore
@Fetch(FetchMode.SUBSELECT)
@OneToMany(mappedBy = "partner", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private Set<Scope<?, ?>> scopes;
@JsonIgnore
@OneToMany(mappedBy = "scopePartner", fetch = FetchType.LAZY)
private List<OrderEntry> orderEntries;
@Override
public void setScopes(Set<Scope<?, ?>> scopes) {
if (this.scopes != null) {
this.scopes.forEach(scope -> scope.setPartner(null));
}
this.scopes = scopes;
this.scopes.forEach(scope -> scope.setPartner(this));
}
@Override
public boolean isGlobal() {
return false;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment