dev comment logic for check max users
Test Workflow / test (push) Successful in 4s

This commit is contained in:
vladp
2026-02-20 20:51:14 +07:00
parent 86e0e0619f
commit 5551d84bf7
30 changed files with 1167 additions and 3 deletions
@@ -0,0 +1,50 @@
package ru.soune.nocopy.entity.payout;
import jakarta.persistence.Column;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
import lombok.*;
@Entity
@DiscriminatorValue("BANK_TRANSFER")
@NoArgsConstructor
@AllArgsConstructor
@Getter @Setter
public class BankTransferPayoutMethod extends PayoutMethod {
@Column(name = "bank_name")
private String bankName;
@Column(name = "bic")
private String bic;
@Column(name = "correspondent_account")
private String correspondentAccount;
@Column(name = "account_number")
private String accountNumber;
@Column(name = "account_holder")
private String accountHolder;
@Column(name = "inn")
private String inn;
@Column(name = "kpp")
private String kpp;
@Override
public String getDisplayName() {
return String.format("Tax %s in %s", getMaskedAccount(), bankName);
}
@Override
public String getMaskedDetails() {
return getMaskedAccount();
}
private String getMaskedAccount() {
if (accountNumber == null || accountNumber.length() < 4) return "";
return "..." + accountNumber.substring(accountNumber.length() - 4);
}
}
@@ -0,0 +1,37 @@
package ru.soune.nocopy.entity.payout;
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
import lombok.*;
import ru.soune.nocopy.entity.payout.convertor.CardNumberEncryptor;
@Entity
@DiscriminatorValue("CARD")
@NoArgsConstructor
@AllArgsConstructor
@Setter @Getter
public class CardPayoutMethod extends PayoutMethod {
@Convert(converter = CardNumberEncryptor.class)
@Column(name = "card_number", nullable = false)
private String cardNumber;
@Column(name = "card_hash", unique = true)
private String cardHash;
@Override
public String getDisplayName() {
return "Card: " + getMaskedCardNumber();
}
@Override
public String getMaskedDetails() {
return getMaskedCardNumber();
}
private String getMaskedCardNumber() {
if (cardNumber == null || cardNumber.length() < 4) return "";
return "****" + cardNumber.substring(cardNumber.length() - 4);
}
}
@@ -0,0 +1,19 @@
package ru.soune.nocopy.entity.payout;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
@Entity
@DiscriminatorValue("INTERNAL")
public class InternalPayoutMethod extends PayoutMethod {
@Override
public String getDisplayName() {
return "Internal balance";
}
@Override
public String getMaskedDetails() {
return "Account balance";
}
}
@@ -0,0 +1,48 @@
package ru.soune.nocopy.entity.payout;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import ru.soune.nocopy.entity.user.User;
import java.time.LocalDateTime;
@Entity
@Table(name = "payout_methods")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "method_type")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PayoutMethod {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@Column(name = "method_type", insertable = false, updatable = false)
private String methodType;
@Column(name = "is_default")
private boolean isDefault;
@CreationTimestamp
private LocalDateTime createdAt;
@UpdateTimestamp
private LocalDateTime updatedAt;
public String getDisplayName() {
return null;
}
public String getMaskedDetails() {
return null;
}
}
@@ -0,0 +1,64 @@
package ru.soune.nocopy.entity.payout;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import ru.soune.nocopy.entity.user.User;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Entity
@Table(name = "payout_requests")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class PayoutRequest {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "payout_method_id")
@JsonIgnore
private PayoutMethod payoutMethod;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
@JsonIgnore
private User user;
@Column(nullable = false)
private BigDecimal amount;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private PayoutRequestStatus status;
@Enumerated(EnumType.STRING)
@Column(nullable = false, name = "payout_type")
@JsonIgnore
private PayoutType payoutType;
@Column(name = "process_at")
private LocalDateTime processAt;
@Column(name = "completed_at")
private LocalDateTime completedAt;
@Column(name = "rejection_reason")
private String rejectionReason;
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}
@@ -0,0 +1,9 @@
package ru.soune.nocopy.entity.payout;
public enum PayoutRequestStatus {
PENDING,
PROCESSING,
COMPLETED,
REJECTED,
CANCELLED
}
@@ -0,0 +1,17 @@
package ru.soune.nocopy.entity.payout;
public enum PayoutType {
CARD("card"),
BANK_TRANSFER("bank_transfer"),
INTERNAL("internal");
private final String displayName;
PayoutType(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
}
@@ -0,0 +1,74 @@
package ru.soune.nocopy.entity.payout.convertor;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
@Converter
public class CardNumberEncryptor implements AttributeConverter<String, String> {
private static final String ALGORITHM = "AES/GCM/NoPadding";
private static final int GCM_TAG_LENGTH = 128;
private static final int GCM_IV_LENGTH = 12;
private static final byte[] FIXED_KEY = "12345678901234567890123456789012".getBytes();
@Override
public String convertToDatabaseColumn(String cardNumber) {
if (cardNumber == null) return null;
try {
SecretKeySpec key = new SecretKeySpec(FIXED_KEY, "AES");
byte[] iv = new byte[GCM_IV_LENGTH];
new SecureRandom().nextBytes(iv);
Cipher cipher = Cipher.getInstance(ALGORITHM);
GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec);
byte[] encrypted = cipher.doFinal(cardNumber.getBytes(StandardCharsets.UTF_8));
byte[] result = new byte[iv.length + encrypted.length];
System.arraycopy(iv, 0, result, 0, iv.length);
System.arraycopy(encrypted, 0, result, iv.length, encrypted.length);
return Base64.getEncoder().encodeToString(result);
} catch (Exception e) {
throw new RuntimeException("Failed to encrypt card number", e);
}
}
@Override
public String convertToEntityAttribute(String dbData) {
if (dbData == null) return null;
try {
byte[] decoded = Base64.getDecoder().decode(dbData);
byte[] iv = new byte[GCM_IV_LENGTH];
byte[] encrypted = new byte[decoded.length - GCM_IV_LENGTH];
System.arraycopy(decoded, 0, iv, 0, iv.length);
System.arraycopy(decoded, iv.length, encrypted, 0, encrypted.length);
SecretKeySpec key = new SecretKeySpec(FIXED_KEY, "AES");
Cipher cipher = Cipher.getInstance(ALGORITHM);
GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.DECRYPT_MODE, key, parameterSpec);
byte[] decrypted = cipher.doFinal(encrypted);
return new String(decrypted, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("Failed to decrypt card number", e);
}
}
}
@@ -25,6 +25,9 @@ public class Referral {
@Column(name = "total_income")
private int totalIncome = 0;
@Column(name = "available_income")
private int availableIncome = 0;
@Column(name = "is_active")
private boolean isActive = false;