Donation.java
package org.petify.funding.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.DiscriminatorColumn;
import jakarta.persistence.DiscriminatorType;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Inheritance;
import jakarta.persistence.InheritanceType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.SuperBuilder;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@Entity
@Table(name = "donations")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(
name = "donation_type",
discriminatorType = DiscriminatorType.STRING
)
public abstract class Donation {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(
mappedBy = "donation",
cascade = CascadeType.ALL,
orphanRemoval = true
)
@Builder.Default
private List<Payment> payments = new ArrayList<>();
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private DonationStatus status = DonationStatus.PENDING;
@Column(name = "shelter_id", nullable = false)
private Long shelterId;
@Column(name = "pet_id")
private Long petId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "fundraiser_id")
private Fundraiser fundraiser;
@Column(name = "donor_username")
private String donorUsername;
@Column(name = "donated_at")
private Instant donatedAt;
@JsonProperty("donationType")
@Column(
name = "donation_type",
insertable = false,
updatable = false
)
@Enumerated(EnumType.STRING)
private DonationType donationType;
@Column(
name = "amount",
nullable = false,
precision = 15,
scale = 2
)
private BigDecimal amount;
@Enumerated(EnumType.STRING)
@Column(name = "currency", length = 3, nullable = false)
private Currency currency = Currency.PLN;
@Column(name = "message", length = 500)
private String message;
@Column(name = "anonymous")
private Boolean anonymous = false;
@Column(name = "created_at")
private Instant createdAt;
@Column(name = "updated_at")
private Instant updatedAt;
@Column(name = "cancelled_at")
private Instant cancelledAt;
@Column(name = "refunded_at")
private Instant refundedAt;
@Column(name = "payment_attempts", nullable = false)
@Builder.Default
private Integer paymentAttempts = 0;
@PrePersist
protected void onCreate() {
createdAt = Instant.now();
updatedAt = Instant.now();
if (paymentAttempts == null) {
paymentAttempts = 0;
}
if (this instanceof MaterialDonation) {
this.donationType = DonationType.MATERIAL;
MaterialDonation md = (MaterialDonation) this;
md.recalculateAmount();
this.amount = md.getAmount();
this.currency = md.getCurrency();
} else if (this instanceof MonetaryDonation) {
this.donationType = DonationType.MONEY;
}
}
@PreUpdate
protected void onUpdate() {
updatedAt = Instant.now();
if (status == DonationStatus.COMPLETED && donatedAt == null) {
donatedAt = Instant.now();
}
if (status == DonationStatus.CANCELLED && cancelledAt == null) {
cancelledAt = Instant.now();
}
if (status == DonationStatus.REFUNDED && refundedAt == null) {
refundedAt = Instant.now();
}
if (this instanceof MaterialDonation) {
this.donationType = DonationType.MATERIAL;
MaterialDonation md = (MaterialDonation) this;
md.recalculateAmount();
this.amount = md.getAmount();
this.currency = md.getCurrency();
} else if (this instanceof MonetaryDonation) {
this.donationType = DonationType.MONEY;
}
}
public boolean isCompleted() {
return status == DonationStatus.COMPLETED;
}
public boolean isCancelled() {
return status == DonationStatus.CANCELLED;
}
public boolean isFailed() {
return status == DonationStatus.FAILED;
}
public boolean hasPendingPayments() {
if (payments == null) {
return false;
}
return payments.stream()
.anyMatch(payment -> payment.getStatus() == PaymentStatus.PENDING
|| payment.getStatus() == PaymentStatus.PROCESSING);
}
public boolean hasSuccessfulPayment() {
if (payments == null) {
return false;
}
return payments.stream()
.anyMatch(payment -> payment.getStatus() == PaymentStatus.SUCCEEDED);
}
public boolean canAcceptNewPayment() {
if (status == DonationStatus.COMPLETED || status == DonationStatus.CANCELLED
|| status == DonationStatus.REFUNDED) {
return false;
}
if (paymentAttempts >= 3) {
return false;
}
return !hasPendingPayments();
}
public boolean canBeCancelled() {
return status == DonationStatus.PENDING && !hasSuccessfulPayment();
}
public boolean canBeRefunded() {
return status == DonationStatus.COMPLETED && hasSuccessfulPayment();
}
public void incrementPaymentAttempts() {
this.paymentAttempts = (this.paymentAttempts == null ? 0 : this.paymentAttempts) + 1;
}
public boolean hasReachedMaxPaymentAttempts() {
return this.paymentAttempts != null && this.paymentAttempts >= 3;
}
public BigDecimal getTotalPaidAmount() {
if (payments == null) {
return BigDecimal.ZERO;
}
return payments.stream()
.filter(payment -> payment.getStatus() == PaymentStatus.SUCCEEDED)
.map(Payment::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
public BigDecimal getTotalFeeAmount() {
if (payments == null) {
return BigDecimal.ZERO;
}
return payments.stream()
.filter(payment -> payment.getStatus() == PaymentStatus.SUCCEEDED)
.map(payment -> payment.getFeeAmount() != null ? payment.getFeeAmount() : BigDecimal.ZERO)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
public BigDecimal getNetAmount() {
return getTotalPaidAmount().subtract(getTotalFeeAmount());
}
public boolean isDonorUsernameEmail() {
return donorUsername != null && donorUsername.contains("@");
}
}