Initial setup, cleanup, VPS setup
All checks were successful
Deploy to VPS / deploy (push) Successful in 52s
All checks were successful
Deploy to VPS / deploy (push) Successful in 52s
This commit is contained in:
22
src/main/java/com/honey/honey/HoneyBackendApplication.java
Normal file
22
src/main/java/com/honey/honey/HoneyBackendApplication.java
Normal file
@@ -0,0 +1,22 @@
|
||||
package com.honey.honey;
|
||||
|
||||
import com.honey.honey.config.ConfigLoader;
|
||||
import com.honey.honey.config.TelegramProperties;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableScheduling
|
||||
@EnableAsync
|
||||
@EnableConfigurationProperties({TelegramProperties.class})
|
||||
public class HoneyBackendApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication app = new SpringApplication(HoneyBackendApplication.class);
|
||||
app.addListeners(new ConfigLoader());
|
||||
app.run(args);
|
||||
}
|
||||
}
|
||||
|
||||
138
src/main/java/com/honey/honey/config/AdminSecurityConfig.java
Normal file
138
src/main/java/com/honey/honey/config/AdminSecurityConfig.java
Normal file
@@ -0,0 +1,138 @@
|
||||
package com.honey.honey.config;
|
||||
|
||||
import com.honey.honey.security.admin.AdminDetailsService;
|
||||
import com.honey.honey.security.admin.JwtAuthenticationFilter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.ProviderManager;
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableMethodSecurity
|
||||
@RequiredArgsConstructor
|
||||
public class AdminSecurityConfig {
|
||||
|
||||
@Value("${FRONTEND_URL:}")
|
||||
private String frontendUrl;
|
||||
|
||||
private final JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||
private final AdminDetailsService adminDetailsService;
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DaoAuthenticationProvider adminAuthenticationProvider() {
|
||||
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
|
||||
authProvider.setUserDetailsService(adminDetailsService);
|
||||
authProvider.setPasswordEncoder(passwordEncoder());
|
||||
return authProvider;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationManager adminAuthenticationManager() {
|
||||
return new ProviderManager(adminAuthenticationProvider());
|
||||
}
|
||||
|
||||
/**
|
||||
* Swagger/OpenAPI docs: permitAll with highest precedence so the default Spring Boot chain
|
||||
* (which requires auth for /**) never handles these paths. Includes webjars and resources
|
||||
* so the UI can load CSS/JS.
|
||||
*/
|
||||
@Bean
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
public SecurityFilterChain swaggerSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
RequestMatcher swaggerMatcher = new OrRequestMatcher(
|
||||
new AntPathRequestMatcher("/swagger-ui/**"),
|
||||
new AntPathRequestMatcher("/swagger-ui.html"),
|
||||
new AntPathRequestMatcher("/v3/api-docs"),
|
||||
new AntPathRequestMatcher("/v3/api-docs/**"),
|
||||
new AntPathRequestMatcher("/webjars/**"),
|
||||
new AntPathRequestMatcher("/swagger-resources/**"),
|
||||
new AntPathRequestMatcher("/configuration/**")
|
||||
);
|
||||
http
|
||||
.securityMatcher(swaggerMatcher)
|
||||
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(2)
|
||||
public SecurityFilterChain adminSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.securityMatcher("/api/admin/**")
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/api/admin/login").permitAll()
|
||||
.requestMatchers("/api/admin/users/**").hasAnyRole("ADMIN", "GAME_ADMIN")
|
||||
.requestMatchers("/api/admin/payments/**").hasAnyRole("ADMIN", "GAME_ADMIN")
|
||||
.requestMatchers("/api/admin/payouts/**").hasAnyRole("ADMIN", "PAYOUT_SUPPORT", "GAME_ADMIN")
|
||||
.requestMatchers("/api/admin/rooms/**").hasAnyRole("ADMIN", "GAME_ADMIN")
|
||||
.requestMatchers("/api/admin/configurations/**").hasAnyRole("ADMIN", "GAME_ADMIN")
|
||||
.requestMatchers("/api/admin/tickets/**").hasAnyRole("ADMIN", "TICKETS_SUPPORT", "GAME_ADMIN")
|
||||
.requestMatchers("/api/admin/quick-answers/**").hasAnyRole("ADMIN", "TICKETS_SUPPORT", "GAME_ADMIN")
|
||||
.requestMatchers("/api/admin/**").hasRole("ADMIN")
|
||||
.anyRequest().denyAll()
|
||||
)
|
||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
List<String> allowedOrigins = Stream.concat(
|
||||
Stream.of(
|
||||
"http://localhost:5173",
|
||||
"http://localhost:3000"
|
||||
),
|
||||
frontendUrl != null && !frontendUrl.isBlank()
|
||||
? Arrays.stream(frontendUrl.split("\\s*,\\s*")).filter(s -> !s.isBlank())
|
||||
: Stream.empty()
|
||||
).distinct().collect(Collectors.toList());
|
||||
|
||||
CorsConfiguration configuration = new CorsConfiguration();
|
||||
configuration.setAllowedOrigins(allowedOrigins);
|
||||
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
|
||||
configuration.setAllowedHeaders(List.of("*"));
|
||||
configuration.setAllowCredentials(true);
|
||||
configuration.setMaxAge(3600L);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/api/admin/**", configuration);
|
||||
return source;
|
||||
}
|
||||
}
|
||||
|
||||
82
src/main/java/com/honey/honey/config/ConfigLoader.java
Normal file
82
src/main/java/com/honey/honey/config/ConfigLoader.java
Normal file
@@ -0,0 +1,82 @@
|
||||
package com.honey.honey.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
import org.springframework.core.env.MapPropertySource;
|
||||
import org.springframework.core.env.MutablePropertySources;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* Loads configuration from a mounted secret file (tmpfs) with fallback to environment variables.
|
||||
* This allows switching between Railway (env vars) and Inferno (mounted file) deployments.
|
||||
*
|
||||
* Priority:
|
||||
* 1. Mounted file at /run/secrets/honey-config.properties (Inferno)
|
||||
* 2. Environment variables (Railway)
|
||||
*/
|
||||
@Slf4j
|
||||
public class ConfigLoader implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {
|
||||
|
||||
private static final String SECRET_FILE_PATH = "/run/secrets/honey-config.properties";
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
|
||||
ConfigurableEnvironment environment = event.getEnvironment();
|
||||
MutablePropertySources propertySources = environment.getPropertySources();
|
||||
|
||||
Map<String, Object> configProperties = new HashMap<>();
|
||||
|
||||
// Try to load from mounted file first (Inferno deployment)
|
||||
File secretFile = new File(SECRET_FILE_PATH);
|
||||
if (secretFile.exists() && secretFile.isFile() && secretFile.canRead()) {
|
||||
log.info("📁 Loading configuration from mounted secret file: {}", SECRET_FILE_PATH);
|
||||
try {
|
||||
Properties props = new Properties();
|
||||
try (FileInputStream fis = new FileInputStream(secretFile)) {
|
||||
props.load(fis);
|
||||
}
|
||||
|
||||
for (String key : props.stringPropertyNames()) {
|
||||
String value = props.getProperty(key);
|
||||
configProperties.put(key, value);
|
||||
log.debug("Loaded from file: {} = {}", key, maskSensitiveValue(key, value));
|
||||
}
|
||||
log.info("✅ Successfully loaded {} properties from secret file", configProperties.size());
|
||||
} catch (IOException e) {
|
||||
log.warn("⚠️ Failed to read secret file, falling back to environment variables: {}", e.getMessage());
|
||||
}
|
||||
} else {
|
||||
log.info("📝 Secret file not found at {}, using environment variables", SECRET_FILE_PATH);
|
||||
}
|
||||
|
||||
// Environment variables are already loaded by Spring Boot by default
|
||||
// We just add file-based config as a higher priority source if it exists
|
||||
if (!configProperties.isEmpty()) {
|
||||
propertySources.addFirst(new MapPropertySource("secretFileConfig", configProperties));
|
||||
log.info("✅ Configuration loaded: {} properties from file, environment variables as fallback",
|
||||
configProperties.size());
|
||||
} else {
|
||||
log.info("✅ Using environment variables for configuration");
|
||||
}
|
||||
}
|
||||
|
||||
private String maskSensitiveValue(String key, String value) {
|
||||
if (value == null) return "null";
|
||||
if (key.toLowerCase().contains("password") ||
|
||||
key.toLowerCase().contains("token") ||
|
||||
key.toLowerCase().contains("secret") ||
|
||||
key.toLowerCase().contains("key")) {
|
||||
return value.length() > 4 ? value.substring(0, 2) + "***" + value.substring(value.length() - 2) : "***";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
53
src/main/java/com/honey/honey/config/CorsConfig.java
Normal file
53
src/main/java/com/honey/honey/config/CorsConfig.java
Normal file
@@ -0,0 +1,53 @@
|
||||
package com.honey.honey.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@Configuration
|
||||
public class CorsConfig {
|
||||
|
||||
@Value("${FRONTEND_URL:}")
|
||||
private String frontendUrl;
|
||||
|
||||
private static final List<String> ADDITIONAL_ORIGINS = Arrays.asList(
|
||||
"https://honey-test-fe-production.up.railway.app",
|
||||
"https://web.telegram.org",
|
||||
"https://webk.telegram.org",
|
||||
"https://t.me"
|
||||
);
|
||||
|
||||
@Bean
|
||||
public WebMvcConfigurer corsConfigurer() {
|
||||
return new WebMvcConfigurer() {
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
List<String> origins = Stream.concat(
|
||||
Arrays.stream(frontendUrl != null && !frontendUrl.isBlank()
|
||||
? frontendUrl.split("\\s*,\\s*")
|
||||
: new String[]{}),
|
||||
ADDITIONAL_ORIGINS.stream()
|
||||
).filter(s -> s != null && !s.isBlank()).distinct().collect(Collectors.toList());
|
||||
|
||||
if (origins.isEmpty()) {
|
||||
origins = ADDITIONAL_ORIGINS;
|
||||
}
|
||||
|
||||
registry.addMapping("/**")
|
||||
.allowedOrigins(origins.toArray(new String[0]))
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||
.allowedHeaders("*")
|
||||
.allowCredentials(true)
|
||||
.maxAge(3600);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
61
src/main/java/com/honey/honey/config/LocaleConfig.java
Normal file
61
src/main/java/com/honey/honey/config/LocaleConfig.java
Normal file
@@ -0,0 +1,61 @@
|
||||
package com.honey.honey.config;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.support.ResourceBundleMessageSource;
|
||||
import org.springframework.web.servlet.LocaleResolver;
|
||||
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
@Configuration
|
||||
public class LocaleConfig {
|
||||
|
||||
// Supported languages
|
||||
public static final List<String> SUPPORTED_LANGUAGES = Arrays.asList(
|
||||
"EN", "RU", "DE", "IT", "NL", "PL", "FR", "ES", "ID", "TR"
|
||||
);
|
||||
|
||||
@Bean
|
||||
public MessageSource messageSource() {
|
||||
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
|
||||
messageSource.setBasename("messages");
|
||||
messageSource.setDefaultEncoding("UTF-8");
|
||||
messageSource.setFallbackToSystemLocale(false);
|
||||
return messageSource;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LocaleResolver localeResolver() {
|
||||
AcceptHeaderLocaleResolver resolver = new AcceptHeaderLocaleResolver();
|
||||
resolver.setDefaultLocale(Locale.ENGLISH);
|
||||
return resolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts language code (EN, RU, etc.) to Locale.
|
||||
*/
|
||||
public static Locale languageCodeToLocale(String languageCode) {
|
||||
if (languageCode == null || languageCode.isEmpty() || "XX".equals(languageCode)) {
|
||||
return Locale.ENGLISH;
|
||||
}
|
||||
|
||||
String upperCode = languageCode.toUpperCase();
|
||||
// Handle special cases
|
||||
switch (upperCode) {
|
||||
case "ID":
|
||||
return new Locale("id"); // Indonesian
|
||||
default:
|
||||
try {
|
||||
return new Locale(upperCode.toLowerCase());
|
||||
} catch (Exception e) {
|
||||
return Locale.ENGLISH; // Default fallback
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
33
src/main/java/com/honey/honey/config/OpenApiConfig.java
Normal file
33
src/main/java/com/honey/honey/config/OpenApiConfig.java
Normal file
@@ -0,0 +1,33 @@
|
||||
package com.honey.honey.config;
|
||||
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springdoc.core.models.GroupedOpenApi;
|
||||
|
||||
/**
|
||||
* OpenAPI / Swagger configuration for the public API only.
|
||||
* Admin endpoints (/api/admin/**) are excluded from the documentation.
|
||||
*/
|
||||
@Configuration
|
||||
public class OpenApiConfig {
|
||||
|
||||
@Bean
|
||||
public GroupedOpenApi publicApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("public")
|
||||
.pathsToMatch("/**")
|
||||
.pathsToExclude("/api/admin/**")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public OpenAPI honeyOpenAPI() {
|
||||
return new OpenAPI()
|
||||
.info(new Info()
|
||||
.title("Honey Public API")
|
||||
.description("API for the Honey frontend. Admin panel endpoints are not included.")
|
||||
.version("1.0"));
|
||||
}
|
||||
}
|
||||
35
src/main/java/com/honey/honey/config/TelegramProperties.java
Normal file
35
src/main/java/com/honey/honey/config/TelegramProperties.java
Normal file
@@ -0,0 +1,35 @@
|
||||
package com.honey.honey.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "telegram")
|
||||
@Data
|
||||
public class TelegramProperties {
|
||||
private String botToken;
|
||||
|
||||
/**
|
||||
* Bot token for checking channel membership.
|
||||
* Can be set via environment variable TELEGRAM_CHANNEL_CHECKER_BOT_TOKEN
|
||||
* or in mounted file at /run/secrets/honey-config.properties as telegram.channel-checker-bot-token
|
||||
*/
|
||||
private String channelCheckerBotToken;
|
||||
|
||||
/**
|
||||
* Channel ID for follow tasks (e.g., "@win_spin_news" or numeric ID).
|
||||
* Can be set via environment variable TELEGRAM_FOLLOW_TASK_CHANNEL_ID
|
||||
* or in mounted file at /run/secrets/honey-config.properties as telegram.follow-task-channel-id
|
||||
*/
|
||||
private String followTaskChannelId;
|
||||
|
||||
/**
|
||||
* Channel ID for follow withdrawals channel task (e.g., "@win_spin_withdrawals" or numeric ID).
|
||||
* Can be set via environment variable TELEGRAM_FOLLOW_TASK_CHANNEL_ID_2
|
||||
* or in mounted file at /run/secrets/honey-config.properties as telegram.follow-task-channel-id-2
|
||||
*/
|
||||
private String followTaskChannelId2;
|
||||
}
|
||||
|
||||
|
||||
51
src/main/java/com/honey/honey/config/WebConfig.java
Normal file
51
src/main/java/com/honey/honey/config/WebConfig.java
Normal file
@@ -0,0 +1,51 @@
|
||||
package com.honey.honey.config;
|
||||
|
||||
import com.honey.honey.security.AuthInterceptor;
|
||||
import com.honey.honey.security.RateLimitInterceptor;
|
||||
import com.honey.honey.security.UserRateLimitInterceptor;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
private final AuthInterceptor authInterceptor;
|
||||
private final RateLimitInterceptor rateLimitInterceptor;
|
||||
private final UserRateLimitInterceptor userRateLimitInterceptor;
|
||||
|
||||
@Override
|
||||
public void addInterceptors(org.springframework.web.servlet.config.annotation.InterceptorRegistry registry) {
|
||||
// NOTE: Rate limiting is NOT applied to Telegram webhook endpoint
|
||||
// Telegram sends webhooks from multiple IPs and we need to process all updates, especially payments
|
||||
// Rate limiting interceptor is only for bot registration endpoint (if needed elsewhere)
|
||||
|
||||
// User session interceptor for all other authenticated endpoints
|
||||
registry.addInterceptor(authInterceptor)
|
||||
.excludePathPatterns(
|
||||
"/ping",
|
||||
"/actuator/**",
|
||||
"/api/auth/tma/session", // Session creation endpoint doesn't require auth
|
||||
"/api/telegram/webhook/**", // Telegram webhook (token in path, validated in controller)
|
||||
"/avatars/**", // Avatar static files don't require auth (served by Nginx in production)
|
||||
"/api/check_user/**", // User check endpoint for external applications (open endpoint)
|
||||
"/api/deposit_webhook/**", // 3rd party deposit completion webhook (token in path, no auth)
|
||||
"/api/notify_broadcast/**", // Notify broadcast start/stop (token in path, no auth)
|
||||
"/api/admin/**", // Admin endpoints are handled by Spring Security
|
||||
// Swagger / OpenAPI docs (no auth required for documentation)
|
||||
"/swagger-ui/**",
|
||||
"/swagger-ui.html",
|
||||
"/v3/api-docs",
|
||||
"/v3/api-docs/**",
|
||||
"/webjars/**",
|
||||
"/swagger-resources/**",
|
||||
"/configuration/**"
|
||||
);
|
||||
|
||||
// User-based rate limiting for payment creation and payout creation (applied after auth interceptor)
|
||||
registry.addInterceptor(userRateLimitInterceptor)
|
||||
.addPathPatterns("/api/payments/create", "/api/payouts");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
package com.honey.honey.controller;
|
||||
|
||||
import com.honey.honey.model.Payment;
|
||||
import com.honey.honey.model.Payout;
|
||||
import com.honey.honey.repository.PaymentRepository;
|
||||
import com.honey.honey.repository.PayoutRepository;
|
||||
import com.honey.honey.repository.UserARepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/analytics")
|
||||
@RequiredArgsConstructor
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public class AdminAnalyticsController {
|
||||
|
||||
private final UserARepository userARepository;
|
||||
private final PaymentRepository paymentRepository;
|
||||
private final PayoutRepository payoutRepository;
|
||||
|
||||
/**
|
||||
* Get revenue and payout time series data for charts.
|
||||
* @param range Time range: 7d, 30d, 90d, 1y, all
|
||||
* @return Time series data with daily/weekly/monthly aggregation
|
||||
*/
|
||||
@GetMapping("/revenue")
|
||||
public ResponseEntity<Map<String, Object>> getRevenueAnalytics(
|
||||
@RequestParam(defaultValue = "30d") String range) {
|
||||
|
||||
Instant now = Instant.now();
|
||||
Instant startDate;
|
||||
String granularity;
|
||||
|
||||
// Determine start date and granularity based on range
|
||||
switch (range.toLowerCase()) {
|
||||
case "7d":
|
||||
startDate = now.minus(7, ChronoUnit.DAYS);
|
||||
granularity = "daily";
|
||||
break;
|
||||
case "30d":
|
||||
startDate = now.minus(30, ChronoUnit.DAYS);
|
||||
granularity = "daily";
|
||||
break;
|
||||
case "90d":
|
||||
startDate = now.minus(90, ChronoUnit.DAYS);
|
||||
granularity = "daily";
|
||||
break;
|
||||
case "1y":
|
||||
startDate = now.minus(365, ChronoUnit.DAYS);
|
||||
granularity = "weekly";
|
||||
break;
|
||||
case "all":
|
||||
startDate = Instant.ofEpochSecond(0); // All time
|
||||
granularity = "monthly";
|
||||
break;
|
||||
default:
|
||||
startDate = now.minus(30, ChronoUnit.DAYS);
|
||||
granularity = "daily";
|
||||
}
|
||||
|
||||
List<Map<String, Object>> dataPoints = new ArrayList<>();
|
||||
Instant current = startDate;
|
||||
|
||||
while (current.isBefore(now)) {
|
||||
Instant periodEnd;
|
||||
if (granularity.equals("daily")) {
|
||||
periodEnd = current.plus(1, ChronoUnit.DAYS);
|
||||
} else if (granularity.equals("weekly")) {
|
||||
periodEnd = current.plus(7, ChronoUnit.DAYS);
|
||||
} else {
|
||||
periodEnd = current.plus(30, ChronoUnit.DAYS);
|
||||
}
|
||||
|
||||
if (periodEnd.isAfter(now)) {
|
||||
periodEnd = now;
|
||||
}
|
||||
|
||||
// CRYPTO only: revenue and payouts in USD for this period
|
||||
java.math.BigDecimal revenueUsd = paymentRepository.sumUsdAmountByStatusAndUsdAmountNotNullAndCreatedAtBetween(
|
||||
Payment.PaymentStatus.COMPLETED, current, periodEnd).orElse(java.math.BigDecimal.ZERO);
|
||||
java.math.BigDecimal payoutsUsd = payoutRepository.sumUsdAmountByTypeCryptoAndStatusAndCreatedAtBetween(
|
||||
Payout.PayoutStatus.COMPLETED, current, periodEnd).orElse(java.math.BigDecimal.ZERO);
|
||||
java.math.BigDecimal netRevenueUsd = revenueUsd.subtract(payoutsUsd);
|
||||
|
||||
Map<String, Object> point = new HashMap<>();
|
||||
point.put("date", current.getEpochSecond());
|
||||
point.put("revenue", revenueUsd);
|
||||
point.put("payouts", payoutsUsd);
|
||||
point.put("netRevenue", netRevenueUsd);
|
||||
|
||||
dataPoints.add(point);
|
||||
|
||||
current = periodEnd;
|
||||
}
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("range", range);
|
||||
response.put("granularity", granularity);
|
||||
response.put("data", dataPoints);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user activity time series data (registrations, active players, rounds).
|
||||
* @param range Time range: 7d, 30d, 90d, 1y, all
|
||||
* @return Time series data
|
||||
*/
|
||||
@GetMapping("/activity")
|
||||
public ResponseEntity<Map<String, Object>> getActivityAnalytics(
|
||||
@RequestParam(defaultValue = "30d") String range) {
|
||||
|
||||
Instant now = Instant.now();
|
||||
Instant startDate;
|
||||
String granularity;
|
||||
|
||||
switch (range.toLowerCase()) {
|
||||
case "7d":
|
||||
startDate = now.minus(7, ChronoUnit.DAYS);
|
||||
granularity = "daily";
|
||||
break;
|
||||
case "30d":
|
||||
startDate = now.minus(30, ChronoUnit.DAYS);
|
||||
granularity = "daily";
|
||||
break;
|
||||
case "90d":
|
||||
startDate = now.minus(90, ChronoUnit.DAYS);
|
||||
granularity = "daily";
|
||||
break;
|
||||
case "1y":
|
||||
startDate = now.minus(365, ChronoUnit.DAYS);
|
||||
granularity = "weekly";
|
||||
break;
|
||||
case "all":
|
||||
startDate = Instant.ofEpochSecond(0);
|
||||
granularity = "monthly";
|
||||
break;
|
||||
default:
|
||||
startDate = now.minus(30, ChronoUnit.DAYS);
|
||||
granularity = "daily";
|
||||
}
|
||||
|
||||
List<Map<String, Object>> dataPoints = new ArrayList<>();
|
||||
Instant current = startDate;
|
||||
|
||||
while (current.isBefore(now)) {
|
||||
Instant periodEnd;
|
||||
if (granularity.equals("daily")) {
|
||||
periodEnd = current.plus(1, ChronoUnit.DAYS);
|
||||
} else if (granularity.equals("weekly")) {
|
||||
periodEnd = current.plus(7, ChronoUnit.DAYS);
|
||||
} else {
|
||||
periodEnd = current.plus(30, ChronoUnit.DAYS);
|
||||
}
|
||||
|
||||
if (periodEnd.isAfter(now)) {
|
||||
periodEnd = now;
|
||||
}
|
||||
|
||||
// Convert to Unix timestamps for UserA queries
|
||||
int periodStartTs = (int) current.getEpochSecond();
|
||||
int periodEndTs = (int) periodEnd.getEpochSecond();
|
||||
|
||||
// Count new registrations in this period (between current and periodEnd)
|
||||
long newUsers = userARepository.countByDateRegBetween(periodStartTs, periodEndTs);
|
||||
|
||||
// Count active players (logged in) in this period
|
||||
long activePlayers = userARepository.countByDateLoginBetween(periodStartTs, periodEndTs);
|
||||
|
||||
Map<String, Object> point = new HashMap<>();
|
||||
point.put("date", current.getEpochSecond());
|
||||
point.put("newUsers", newUsers);
|
||||
point.put("activePlayers", activePlayers);
|
||||
point.put("rounds", 0L);
|
||||
|
||||
dataPoints.add(point);
|
||||
|
||||
current = periodEnd;
|
||||
}
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("range", range);
|
||||
response.put("granularity", granularity);
|
||||
response.put("data", dataPoints);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
package com.honey.honey.controller;
|
||||
|
||||
import com.honey.honey.model.Payment;
|
||||
import com.honey.honey.model.Payout;
|
||||
import com.honey.honey.model.SupportTicket;
|
||||
import com.honey.honey.model.UserA;
|
||||
import com.honey.honey.repository.PaymentRepository;
|
||||
import com.honey.honey.repository.PayoutRepository;
|
||||
import com.honey.honey.repository.SupportTicketRepository;
|
||||
import com.honey.honey.repository.UserARepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/dashboard")
|
||||
@RequiredArgsConstructor
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public class AdminDashboardController {
|
||||
|
||||
private final UserARepository userARepository;
|
||||
private final PaymentRepository paymentRepository;
|
||||
private final PayoutRepository payoutRepository;
|
||||
private final SupportTicketRepository supportTicketRepository;
|
||||
|
||||
@GetMapping("/stats")
|
||||
public ResponseEntity<Map<String, Object>> getDashboardStats() {
|
||||
Instant now = Instant.now();
|
||||
Instant todayStart = now.truncatedTo(ChronoUnit.DAYS);
|
||||
Instant weekStart = now.minus(7, ChronoUnit.DAYS);
|
||||
Instant monthStart = now.minus(30, ChronoUnit.DAYS);
|
||||
Instant dayAgo = now.minus(24, ChronoUnit.HOURS);
|
||||
Instant weekAgo = now.minus(7, ChronoUnit.DAYS);
|
||||
Instant monthAgo = now.minus(30, ChronoUnit.DAYS);
|
||||
|
||||
// Convert to Unix timestamps (seconds) for UserA date fields
|
||||
int todayStartTs = (int) todayStart.getEpochSecond();
|
||||
int weekStartTs = (int) weekStart.getEpochSecond();
|
||||
int monthStartTs = (int) monthStart.getEpochSecond();
|
||||
int dayAgoTs = (int) dayAgo.getEpochSecond();
|
||||
int weekAgoTs = (int) weekAgo.getEpochSecond();
|
||||
int monthAgoTs = (int) monthAgo.getEpochSecond();
|
||||
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
|
||||
// Total Users
|
||||
long totalUsers = userARepository.count();
|
||||
long newUsersToday = userARepository.countByDateRegAfter(todayStartTs);
|
||||
long newUsersWeek = userARepository.countByDateRegAfter(weekStartTs);
|
||||
long newUsersMonth = userARepository.countByDateRegAfter(monthStartTs);
|
||||
|
||||
// Active Players (users who logged in recently)
|
||||
long activePlayers24h = userARepository.countByDateLoginAfter(dayAgoTs);
|
||||
long activePlayers7d = userARepository.countByDateLoginAfter(weekAgoTs);
|
||||
long activePlayers30d = userARepository.countByDateLoginAfter(monthAgoTs);
|
||||
|
||||
// Revenue (from completed payments) - in Stars
|
||||
int totalRevenue = paymentRepository.sumStarsAmountByStatus(Payment.PaymentStatus.COMPLETED)
|
||||
.orElse(0);
|
||||
int revenueToday = paymentRepository.sumStarsAmountByStatusAndCreatedAtAfter(
|
||||
Payment.PaymentStatus.COMPLETED, todayStart).orElse(0);
|
||||
int revenueWeek = paymentRepository.sumStarsAmountByStatusAndCreatedAtAfter(
|
||||
Payment.PaymentStatus.COMPLETED, weekStart).orElse(0);
|
||||
int revenueMonth = paymentRepository.sumStarsAmountByStatusAndCreatedAtAfter(
|
||||
Payment.PaymentStatus.COMPLETED, monthStart).orElse(0);
|
||||
|
||||
// Payouts (from completed payouts) - in Stars
|
||||
int totalPayouts = payoutRepository.sumStarsAmountByStatus(Payout.PayoutStatus.COMPLETED)
|
||||
.orElse(0);
|
||||
int payoutsToday = payoutRepository.sumStarsAmountByStatusAndCreatedAtAfter(
|
||||
Payout.PayoutStatus.COMPLETED, todayStart).orElse(0);
|
||||
int payoutsWeek = payoutRepository.sumStarsAmountByStatusAndCreatedAtAfter(
|
||||
Payout.PayoutStatus.COMPLETED, weekStart).orElse(0);
|
||||
int payoutsMonth = payoutRepository.sumStarsAmountByStatusAndCreatedAtAfter(
|
||||
Payout.PayoutStatus.COMPLETED, monthStart).orElse(0);
|
||||
|
||||
// Net Revenue (in Stars)
|
||||
int netRevenue = totalRevenue - totalPayouts;
|
||||
int netRevenueToday = revenueToday - payoutsToday;
|
||||
int netRevenueWeek = revenueWeek - payoutsWeek;
|
||||
int netRevenueMonth = revenueMonth - payoutsMonth;
|
||||
|
||||
// CRYPTO only: revenue and payouts in USD (for dashboard / financial analytics)
|
||||
BigDecimal cryptoRevenueTotal = paymentRepository.sumUsdAmountByStatusAndUsdAmountNotNull(Payment.PaymentStatus.COMPLETED).orElse(BigDecimal.ZERO);
|
||||
BigDecimal cryptoRevenueToday = paymentRepository.sumUsdAmountByStatusAndUsdAmountNotNullAndCreatedAtAfter(Payment.PaymentStatus.COMPLETED, todayStart).orElse(BigDecimal.ZERO);
|
||||
BigDecimal cryptoRevenueWeek = paymentRepository.sumUsdAmountByStatusAndUsdAmountNotNullAndCreatedAtAfter(Payment.PaymentStatus.COMPLETED, weekStart).orElse(BigDecimal.ZERO);
|
||||
BigDecimal cryptoRevenueMonth = paymentRepository.sumUsdAmountByStatusAndUsdAmountNotNullAndCreatedAtAfter(Payment.PaymentStatus.COMPLETED, monthStart).orElse(BigDecimal.ZERO);
|
||||
BigDecimal cryptoPayoutsTotal = payoutRepository.sumUsdAmountByTypeCryptoAndStatus(Payout.PayoutStatus.COMPLETED).orElse(BigDecimal.ZERO);
|
||||
BigDecimal cryptoPayoutsToday = payoutRepository.sumUsdAmountByTypeCryptoAndStatusAndCreatedAtAfter(Payout.PayoutStatus.COMPLETED, todayStart).orElse(BigDecimal.ZERO);
|
||||
BigDecimal cryptoPayoutsWeek = payoutRepository.sumUsdAmountByTypeCryptoAndStatusAndCreatedAtAfter(Payout.PayoutStatus.COMPLETED, weekStart).orElse(BigDecimal.ZERO);
|
||||
BigDecimal cryptoPayoutsMonth = payoutRepository.sumUsdAmountByTypeCryptoAndStatusAndCreatedAtAfter(Payout.PayoutStatus.COMPLETED, monthStart).orElse(BigDecimal.ZERO);
|
||||
BigDecimal cryptoNetRevenueTotal = cryptoRevenueTotal.subtract(cryptoPayoutsTotal);
|
||||
BigDecimal cryptoNetRevenueToday = cryptoRevenueToday.subtract(cryptoPayoutsToday);
|
||||
BigDecimal cryptoNetRevenueWeek = cryptoRevenueWeek.subtract(cryptoPayoutsWeek);
|
||||
BigDecimal cryptoNetRevenueMonth = cryptoRevenueMonth.subtract(cryptoPayoutsMonth);
|
||||
|
||||
// Support Tickets
|
||||
long openTickets = supportTicketRepository.countByStatus(SupportTicket.TicketStatus.OPENED);
|
||||
// Count tickets closed today
|
||||
long ticketsResolvedToday = supportTicketRepository.findAll().stream()
|
||||
.filter(t -> t.getStatus() == SupportTicket.TicketStatus.CLOSED &&
|
||||
t.getUpdatedAt() != null &&
|
||||
t.getUpdatedAt().isAfter(todayStart))
|
||||
.count();
|
||||
|
||||
// Build response
|
||||
stats.put("users", Map.of(
|
||||
"total", totalUsers,
|
||||
"newToday", newUsersToday,
|
||||
"newWeek", newUsersWeek,
|
||||
"newMonth", newUsersMonth
|
||||
));
|
||||
|
||||
stats.put("activePlayers", Map.of(
|
||||
"last24h", activePlayers24h,
|
||||
"last7d", activePlayers7d,
|
||||
"last30d", activePlayers30d
|
||||
));
|
||||
|
||||
stats.put("revenue", Map.of(
|
||||
"total", totalRevenue,
|
||||
"today", revenueToday,
|
||||
"week", revenueWeek,
|
||||
"month", revenueMonth
|
||||
));
|
||||
|
||||
stats.put("payouts", Map.of(
|
||||
"total", totalPayouts,
|
||||
"today", payoutsToday,
|
||||
"week", payoutsWeek,
|
||||
"month", payoutsMonth
|
||||
));
|
||||
|
||||
stats.put("netRevenue", Map.of(
|
||||
"total", netRevenue,
|
||||
"today", netRevenueToday,
|
||||
"week", netRevenueWeek,
|
||||
"month", netRevenueMonth
|
||||
));
|
||||
|
||||
Map<String, Object> crypto = new HashMap<>();
|
||||
crypto.put("revenueUsd", cryptoRevenueTotal);
|
||||
crypto.put("revenueUsdToday", cryptoRevenueToday);
|
||||
crypto.put("revenueUsdWeek", cryptoRevenueWeek);
|
||||
crypto.put("revenueUsdMonth", cryptoRevenueMonth);
|
||||
crypto.put("payoutsUsd", cryptoPayoutsTotal);
|
||||
crypto.put("payoutsUsdToday", cryptoPayoutsToday);
|
||||
crypto.put("payoutsUsdWeek", cryptoPayoutsWeek);
|
||||
crypto.put("payoutsUsdMonth", cryptoPayoutsMonth);
|
||||
crypto.put("profitUsd", cryptoNetRevenueTotal);
|
||||
crypto.put("profitUsdToday", cryptoNetRevenueToday);
|
||||
crypto.put("profitUsdWeek", cryptoNetRevenueWeek);
|
||||
crypto.put("profitUsdMonth", cryptoNetRevenueMonth);
|
||||
stats.put("crypto", crypto);
|
||||
|
||||
stats.put("rounds", Map.of(
|
||||
"total", 0L,
|
||||
"today", 0L,
|
||||
"week", 0L,
|
||||
"month", 0L,
|
||||
"avgPool", 0
|
||||
));
|
||||
|
||||
stats.put("supportTickets", Map.of(
|
||||
"open", openTickets,
|
||||
"resolvedToday", ticketsResolvedToday
|
||||
));
|
||||
|
||||
return ResponseEntity.ok(stats);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.honey.honey.controller;
|
||||
|
||||
import com.honey.honey.service.FeatureSwitchService;
|
||||
import com.honey.honey.service.FeatureSwitchService.FeatureSwitchDto;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/feature-switches")
|
||||
@RequiredArgsConstructor
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public class AdminFeatureSwitchController {
|
||||
|
||||
private final FeatureSwitchService featureSwitchService;
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<FeatureSwitchDto>> getAll() {
|
||||
return ResponseEntity.ok(featureSwitchService.getAll());
|
||||
}
|
||||
|
||||
@PatchMapping("/{key}")
|
||||
public ResponseEntity<FeatureSwitchDto> update(
|
||||
@PathVariable String key,
|
||||
@RequestBody Map<String, Boolean> body) {
|
||||
Boolean enabled = body != null ? body.get("enabled") : null;
|
||||
if (enabled == null) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
return ResponseEntity.ok(featureSwitchService.setEnabled(key, enabled));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.honey.honey.controller;
|
||||
|
||||
import com.honey.honey.dto.AdminLoginRequest;
|
||||
import com.honey.honey.dto.AdminLoginResponse;
|
||||
import com.honey.honey.service.AdminService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin")
|
||||
@RequiredArgsConstructor
|
||||
public class AdminLoginController {
|
||||
|
||||
private final AdminService adminService;
|
||||
|
||||
@PostMapping("/login")
|
||||
public ResponseEntity<?> login(@RequestBody AdminLoginRequest request) {
|
||||
if (request.getUsername() == null || request.getPassword() == null) {
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||
.body("Username and password are required");
|
||||
}
|
||||
|
||||
Optional<String> tokenOpt = adminService.authenticate(
|
||||
request.getUsername(),
|
||||
request.getPassword()
|
||||
);
|
||||
|
||||
if (tokenOpt.isEmpty()) {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||
.body("Invalid credentials");
|
||||
}
|
||||
|
||||
// Get admin to retrieve role
|
||||
var adminOpt = adminService.getAdminByUsername(request.getUsername());
|
||||
String role = adminOpt.map(admin -> admin.getRole()).orElse("ROLE_ADMIN");
|
||||
|
||||
return ResponseEntity.ok(new AdminLoginResponse(
|
||||
tokenOpt.get(),
|
||||
request.getUsername(),
|
||||
role
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.honey.honey.controller;
|
||||
|
||||
import com.honey.honey.dto.AdminMasterDto;
|
||||
import com.honey.honey.service.AdminMasterService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/masters")
|
||||
@RequiredArgsConstructor
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public class AdminMasterController {
|
||||
|
||||
private final AdminMasterService adminMasterService;
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<AdminMasterDto>> getMasters() {
|
||||
return ResponseEntity.ok(adminMasterService.getMasters());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.honey.honey.controller;
|
||||
|
||||
import com.honey.honey.dto.NotifyBroadcastRequest;
|
||||
import com.honey.honey.service.NotificationBroadcastService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* Admin API to trigger or stop notification broadcast (ADMIN only).
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/notifications")
|
||||
@RequiredArgsConstructor
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public class AdminNotificationController {
|
||||
|
||||
private final NotificationBroadcastService notificationBroadcastService;
|
||||
|
||||
@PostMapping("/send")
|
||||
public ResponseEntity<Void> send(@RequestBody(required = false) NotifyBroadcastRequest body) {
|
||||
NotifyBroadcastRequest req = body != null ? body : new NotifyBroadcastRequest();
|
||||
notificationBroadcastService.runBroadcast(
|
||||
req.getMessage(),
|
||||
req.getImageUrl(),
|
||||
req.getVideoUrl(),
|
||||
req.getUserIdFrom(),
|
||||
req.getUserIdTo(),
|
||||
req.getButtonText(),
|
||||
req.getIgnoreBlocked());
|
||||
return ResponseEntity.accepted().build();
|
||||
}
|
||||
|
||||
@PostMapping("/stop")
|
||||
public ResponseEntity<Void> stop() {
|
||||
notificationBroadcastService.requestStop();
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package com.honey.honey.controller;
|
||||
|
||||
import com.honey.honey.dto.AdminPaymentDto;
|
||||
import com.honey.honey.model.Payment;
|
||||
import com.honey.honey.model.UserA;
|
||||
import com.honey.honey.repository.PaymentRepository;
|
||||
import com.honey.honey.repository.UserARepository;
|
||||
import com.honey.honey.repository.UserDRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/payments")
|
||||
@RequiredArgsConstructor
|
||||
@PreAuthorize("hasAnyRole('ADMIN', 'GAME_ADMIN')")
|
||||
public class AdminPaymentController {
|
||||
|
||||
private final PaymentRepository paymentRepository;
|
||||
private final UserARepository userARepository;
|
||||
private final UserDRepository userDRepository;
|
||||
|
||||
private boolean isGameAdmin() {
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (auth == null || auth.getAuthorities() == null) return false;
|
||||
return auth.getAuthorities().stream()
|
||||
.anyMatch(a -> "ROLE_GAME_ADMIN".equals(a.getAuthority()));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<Map<String, Object>> getPayments(
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "50") int size,
|
||||
@RequestParam(required = false) String sortBy,
|
||||
@RequestParam(required = false) String sortDir,
|
||||
// Filters
|
||||
@RequestParam(required = false) String status,
|
||||
@RequestParam(required = false) Integer userId,
|
||||
@RequestParam(required = false) String dateFrom,
|
||||
@RequestParam(required = false) String dateTo,
|
||||
@RequestParam(required = false) Long amountMin,
|
||||
@RequestParam(required = false) Long amountMax) {
|
||||
|
||||
// Build sort
|
||||
Sort sort = Sort.by("createdAt").descending(); // Default sort
|
||||
if (sortBy != null && !sortBy.trim().isEmpty()) {
|
||||
Sort.Direction direction = "asc".equalsIgnoreCase(sortDir)
|
||||
? Sort.Direction.ASC
|
||||
: Sort.Direction.DESC;
|
||||
sort = Sort.by(direction, sortBy);
|
||||
}
|
||||
|
||||
Pageable pageable = PageRequest.of(page, size, sort);
|
||||
|
||||
List<Integer> masterIds = isGameAdmin() ? userDRepository.findMasterUserIds() : List.of();
|
||||
|
||||
// Build specification
|
||||
Specification<Payment> spec = (root, query, cb) -> {
|
||||
List<Predicate> predicates = new ArrayList<>();
|
||||
|
||||
if (!masterIds.isEmpty()) {
|
||||
predicates.add(cb.not(root.get("userId").in(masterIds)));
|
||||
}
|
||||
if (status != null && !status.trim().isEmpty()) {
|
||||
try {
|
||||
Payment.PaymentStatus paymentStatus = Payment.PaymentStatus.valueOf(status.toUpperCase());
|
||||
predicates.add(cb.equal(root.get("status"), paymentStatus));
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Invalid status, ignore
|
||||
}
|
||||
}
|
||||
|
||||
if (userId != null) {
|
||||
predicates.add(cb.equal(root.get("userId"), userId));
|
||||
}
|
||||
|
||||
// Date range filters would need Instant conversion
|
||||
// For now, we'll skip them or implement if needed
|
||||
|
||||
return cb.and(predicates.toArray(new Predicate[0]));
|
||||
};
|
||||
|
||||
Page<Payment> paymentPage = paymentRepository.findAll(spec, pageable);
|
||||
|
||||
// Fetch user names
|
||||
List<Integer> userIds = paymentPage.getContent().stream()
|
||||
.map(Payment::getUserId)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Map<Integer, String> userNameMap = userARepository.findAllById(userIds).stream()
|
||||
.collect(Collectors.toMap(
|
||||
UserA::getId,
|
||||
u -> u.getTelegramName() != null && !u.getTelegramName().equals("-")
|
||||
? u.getTelegramName()
|
||||
: u.getScreenName()
|
||||
));
|
||||
|
||||
// Convert to DTOs
|
||||
Page<AdminPaymentDto> dtoPage = paymentPage.map(payment -> {
|
||||
String userName = userNameMap.getOrDefault(payment.getUserId(), "Unknown");
|
||||
return AdminPaymentDto.builder()
|
||||
.id(payment.getId())
|
||||
.userId(payment.getUserId())
|
||||
.userName(userName)
|
||||
.orderId(payment.getOrderId())
|
||||
.starsAmount(payment.getStarsAmount())
|
||||
.ticketsAmount(payment.getTicketsAmount())
|
||||
.status(payment.getStatus().name())
|
||||
.telegramPaymentChargeId(payment.getTelegramPaymentChargeId())
|
||||
.telegramProviderPaymentChargeId(payment.getTelegramProviderPaymentChargeId())
|
||||
.createdAt(payment.getCreatedAt())
|
||||
.completedAt(payment.getCompletedAt())
|
||||
.build();
|
||||
});
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("content", dtoPage.getContent());
|
||||
response.put("totalElements", dtoPage.getTotalElements());
|
||||
response.put("totalPages", dtoPage.getTotalPages());
|
||||
response.put("currentPage", dtoPage.getNumber());
|
||||
response.put("size", dtoPage.getSize());
|
||||
response.put("hasNext", dtoPage.hasNext());
|
||||
response.put("hasPrevious", dtoPage.hasPrevious());
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
package com.honey.honey.controller;
|
||||
|
||||
import com.honey.honey.dto.AdminPayoutDto;
|
||||
import com.honey.honey.model.Payout;
|
||||
import com.honey.honey.model.UserA;
|
||||
import com.honey.honey.model.UserB;
|
||||
import com.honey.honey.repository.PayoutRepository;
|
||||
import com.honey.honey.repository.UserARepository;
|
||||
import com.honey.honey.repository.UserBRepository;
|
||||
import com.honey.honey.repository.UserDRepository;
|
||||
import com.honey.honey.service.LocalizationService;
|
||||
import com.honey.honey.service.PayoutService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/payouts")
|
||||
@RequiredArgsConstructor
|
||||
@PreAuthorize("hasAnyRole('ADMIN', 'PAYOUT_SUPPORT', 'GAME_ADMIN')")
|
||||
public class AdminPayoutController {
|
||||
|
||||
private final PayoutRepository payoutRepository;
|
||||
private final UserARepository userARepository;
|
||||
private final UserBRepository userBRepository;
|
||||
private final UserDRepository userDRepository;
|
||||
private final PayoutService payoutService;
|
||||
private final LocalizationService localizationService;
|
||||
|
||||
private boolean isGameAdmin() {
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (auth == null || auth.getAuthorities() == null) return false;
|
||||
return auth.getAuthorities().stream()
|
||||
.anyMatch(a -> "ROLE_GAME_ADMIN".equals(a.getAuthority()));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<Map<String, Object>> getPayouts(
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "50") int size,
|
||||
@RequestParam(required = false) String sortBy,
|
||||
@RequestParam(required = false) String sortDir,
|
||||
// Filters
|
||||
@RequestParam(required = false) String status,
|
||||
@RequestParam(required = false) Integer userId,
|
||||
@RequestParam(required = false) String type) {
|
||||
|
||||
// Build sort
|
||||
Sort sort = Sort.by("createdAt").descending(); // Default sort
|
||||
if (sortBy != null && !sortBy.trim().isEmpty()) {
|
||||
Sort.Direction direction = "asc".equalsIgnoreCase(sortDir)
|
||||
? Sort.Direction.ASC
|
||||
: Sort.Direction.DESC;
|
||||
sort = Sort.by(direction, sortBy);
|
||||
}
|
||||
|
||||
Pageable pageable = PageRequest.of(page, size, sort);
|
||||
|
||||
List<Integer> masterIds = isGameAdmin() ? userDRepository.findMasterUserIds() : List.of();
|
||||
|
||||
// Build specification
|
||||
Specification<Payout> spec = (root, query, cb) -> {
|
||||
List<Predicate> predicates = new ArrayList<>();
|
||||
|
||||
if (!masterIds.isEmpty()) {
|
||||
predicates.add(cb.not(root.get("userId").in(masterIds)));
|
||||
}
|
||||
if (status != null && !status.trim().isEmpty()) {
|
||||
try {
|
||||
Payout.PayoutStatus payoutStatus = Payout.PayoutStatus.valueOf(status.toUpperCase());
|
||||
predicates.add(cb.equal(root.get("status"), payoutStatus));
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Invalid status, ignore
|
||||
}
|
||||
}
|
||||
|
||||
if (userId != null) {
|
||||
predicates.add(cb.equal(root.get("userId"), userId));
|
||||
}
|
||||
|
||||
if (type != null && !type.trim().isEmpty()) {
|
||||
try {
|
||||
Payout.PayoutType payoutType = Payout.PayoutType.valueOf(type.toUpperCase());
|
||||
predicates.add(cb.equal(root.get("type"), payoutType));
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Invalid type, ignore
|
||||
}
|
||||
}
|
||||
|
||||
return cb.and(predicates.toArray(new Predicate[0]));
|
||||
};
|
||||
|
||||
Page<Payout> payoutPage = payoutRepository.findAll(spec, pageable);
|
||||
|
||||
// Fetch user names
|
||||
List<Integer> userIds = payoutPage.getContent().stream()
|
||||
.map(Payout::getUserId)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Map<Integer, String> userNameMap = userARepository.findAllById(userIds).stream()
|
||||
.collect(Collectors.toMap(
|
||||
UserA::getId,
|
||||
u -> u.getTelegramName() != null && !u.getTelegramName().equals("-")
|
||||
? u.getTelegramName()
|
||||
: u.getScreenName()
|
||||
));
|
||||
|
||||
// Convert to DTOs
|
||||
Page<AdminPayoutDto> dtoPage = payoutPage.map(payout -> {
|
||||
String userName = userNameMap.getOrDefault(payout.getUserId(), "Unknown");
|
||||
return AdminPayoutDto.builder()
|
||||
.id(payout.getId())
|
||||
.userId(payout.getUserId())
|
||||
.userName(userName)
|
||||
.username(payout.getUsername())
|
||||
.type(payout.getType().name())
|
||||
.giftName(payout.getGiftName() != null ? payout.getGiftName().name() : null)
|
||||
.total(payout.getTotal())
|
||||
.starsAmount(payout.getStarsAmount())
|
||||
.quantity(payout.getQuantity())
|
||||
.status(payout.getStatus().name())
|
||||
.createdAt(payout.getCreatedAt())
|
||||
.resolvedAt(payout.getResolvedAt())
|
||||
.build();
|
||||
});
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("content", dtoPage.getContent());
|
||||
response.put("totalElements", dtoPage.getTotalElements());
|
||||
response.put("totalPages", dtoPage.getTotalPages());
|
||||
response.put("currentPage", dtoPage.getNumber());
|
||||
response.put("size", dtoPage.getSize());
|
||||
response.put("hasNext", dtoPage.hasNext());
|
||||
response.put("hasPrevious", dtoPage.hasPrevious());
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/complete")
|
||||
@PreAuthorize("hasAnyRole('ADMIN', 'PAYOUT_SUPPORT')")
|
||||
public ResponseEntity<?> completePayout(@PathVariable Long id) {
|
||||
try {
|
||||
Optional<Payout> payoutOpt = payoutRepository.findById(id);
|
||||
if (payoutOpt.isEmpty()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("error", localizationService.getMessage("payout.error.notFound", String.valueOf(id))));
|
||||
}
|
||||
Payout payout = payoutOpt.get();
|
||||
if (payout.getStatus() != Payout.PayoutStatus.PROCESSING) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("error", localizationService.getMessage("payout.error.onlyProcessingCanComplete", payout.getStatus().name())));
|
||||
}
|
||||
payoutService.markPayoutCompleted(id);
|
||||
return ResponseEntity.ok(Map.of("message", "Payout marked as completed"));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of("error", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/cancel")
|
||||
@PreAuthorize("hasAnyRole('ADMIN', 'PAYOUT_SUPPORT')")
|
||||
public ResponseEntity<?> cancelPayout(@PathVariable Long id) {
|
||||
try {
|
||||
Optional<Payout> payoutOpt = payoutRepository.findById(id);
|
||||
if (payoutOpt.isEmpty()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("error", localizationService.getMessage("payout.error.notFound", String.valueOf(id))));
|
||||
}
|
||||
Payout payout = payoutOpt.get();
|
||||
if (payout.getStatus() != Payout.PayoutStatus.PROCESSING) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("error", localizationService.getMessage("payout.error.onlyProcessingCanCancel", payout.getStatus().name())));
|
||||
}
|
||||
payoutService.markPayoutCancelled(id);
|
||||
return ResponseEntity.ok(Map.of("message", "Payout cancelled"));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of("error", e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
package com.honey.honey.controller;
|
||||
|
||||
import com.honey.honey.dto.*;
|
||||
import com.honey.honey.service.AdminPromotionService;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/promotions")
|
||||
@RequiredArgsConstructor
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public class AdminPromotionController {
|
||||
|
||||
private final AdminPromotionService adminPromotionService;
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<AdminPromotionDto>> listPromotions() {
|
||||
return ResponseEntity.ok(adminPromotionService.listPromotions());
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<AdminPromotionDto> getPromotion(@PathVariable int id) {
|
||||
return adminPromotionService.getPromotion(id)
|
||||
.map(ResponseEntity::ok)
|
||||
.orElseGet(() -> ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<AdminPromotionDto> createPromotion(@Valid @RequestBody AdminPromotionRequest request) {
|
||||
try {
|
||||
AdminPromotionDto dto = adminPromotionService.createPromotion(request);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(dto);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<AdminPromotionDto> updatePromotion(
|
||||
@PathVariable int id,
|
||||
@Valid @RequestBody AdminPromotionRequest request) {
|
||||
try {
|
||||
return adminPromotionService.updatePromotion(id, request)
|
||||
.map(ResponseEntity::ok)
|
||||
.orElseGet(() -> ResponseEntity.notFound().build());
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> deletePromotion(@PathVariable int id) {
|
||||
return adminPromotionService.deletePromotion(id)
|
||||
? ResponseEntity.noContent().build()
|
||||
: ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
// --- Rewards ---
|
||||
|
||||
@GetMapping("/{promoId}/rewards")
|
||||
public ResponseEntity<List<AdminPromotionRewardDto>> listRewards(@PathVariable int promoId) {
|
||||
return ResponseEntity.ok(adminPromotionService.listRewards(promoId));
|
||||
}
|
||||
|
||||
@PostMapping("/{promoId}/rewards")
|
||||
public ResponseEntity<AdminPromotionRewardDto> createReward(
|
||||
@PathVariable int promoId,
|
||||
@Valid @RequestBody AdminPromotionRewardRequest request) {
|
||||
try {
|
||||
AdminPromotionRewardDto dto = adminPromotionService.createReward(promoId, request);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(dto);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
}
|
||||
|
||||
@PutMapping("/rewards/{rewardId}")
|
||||
public ResponseEntity<AdminPromotionRewardDto> updateReward(
|
||||
@PathVariable int rewardId,
|
||||
@Valid @RequestBody AdminPromotionRewardRequest request) {
|
||||
try {
|
||||
return adminPromotionService.updateReward(rewardId, request)
|
||||
.map(ResponseEntity::ok)
|
||||
.orElseGet(() -> ResponseEntity.notFound().build());
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteMapping("/rewards/{rewardId}")
|
||||
public ResponseEntity<Void> deleteReward(@PathVariable int rewardId) {
|
||||
return adminPromotionService.deleteReward(rewardId)
|
||||
? ResponseEntity.noContent().build()
|
||||
: ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
// --- Promotion users (leaderboard / results) ---
|
||||
|
||||
@GetMapping("/{promoId}/users")
|
||||
public ResponseEntity<Map<String, Object>> getPromotionUsers(
|
||||
@PathVariable int promoId,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "50") int size,
|
||||
@RequestParam(required = false) String sortBy,
|
||||
@RequestParam(required = false) String sortDir,
|
||||
@RequestParam(required = false) Integer userId) {
|
||||
Page<AdminPromotionUserDto> dtoPage = adminPromotionService.getPromotionUsers(
|
||||
promoId, page, size, sortBy, sortDir, userId);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("content", dtoPage.getContent());
|
||||
response.put("totalElements", dtoPage.getTotalElements());
|
||||
response.put("totalPages", dtoPage.getTotalPages());
|
||||
response.put("currentPage", dtoPage.getNumber());
|
||||
response.put("size", dtoPage.getSize());
|
||||
response.put("hasNext", dtoPage.hasNext());
|
||||
response.put("hasPrevious", dtoPage.hasPrevious());
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@PatchMapping("/{promoId}/users/{userId}/points")
|
||||
public ResponseEntity<AdminPromotionUserDto> updatePromotionUserPoints(
|
||||
@PathVariable int promoId,
|
||||
@PathVariable int userId,
|
||||
@Valid @RequestBody AdminPromotionUserPointsRequest request) {
|
||||
Optional<AdminPromotionUserDto> updated = adminPromotionService.updatePromotionUserPoints(
|
||||
promoId, userId, request.getPoints());
|
||||
return updated.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
package com.honey.honey.controller;
|
||||
|
||||
import com.honey.honey.dto.*;
|
||||
import com.honey.honey.model.Admin;
|
||||
import com.honey.honey.model.SupportMessage;
|
||||
import com.honey.honey.model.SupportTicket;
|
||||
import com.honey.honey.model.UserA;
|
||||
import com.honey.honey.repository.AdminRepository;
|
||||
import com.honey.honey.repository.SupportMessageRepository;
|
||||
import com.honey.honey.repository.SupportTicketRepository;
|
||||
import com.honey.honey.repository.UserARepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/tickets")
|
||||
@RequiredArgsConstructor
|
||||
@PreAuthorize("hasAnyRole('ADMIN', 'TICKETS_SUPPORT', 'GAME_ADMIN')")
|
||||
public class AdminSupportTicketController {
|
||||
|
||||
private final SupportTicketRepository supportTicketRepository;
|
||||
private final SupportMessageRepository supportMessageRepository;
|
||||
private final UserARepository userARepository;
|
||||
private final AdminRepository adminRepository;
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<Map<String, Object>> getTickets(
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "50") int size,
|
||||
@RequestParam(required = false) String status,
|
||||
@RequestParam(required = false) Integer userId,
|
||||
@RequestParam(required = false) String search) {
|
||||
|
||||
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
|
||||
|
||||
// Build specification
|
||||
Specification<SupportTicket> spec = (root, query, cb) -> {
|
||||
List<Predicate> predicates = new ArrayList<>();
|
||||
|
||||
if (status != null && !status.trim().isEmpty()) {
|
||||
try {
|
||||
SupportTicket.TicketStatus ticketStatus = SupportTicket.TicketStatus.valueOf(status.toUpperCase());
|
||||
predicates.add(cb.equal(root.get("status"), ticketStatus));
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Invalid status, ignore
|
||||
}
|
||||
}
|
||||
|
||||
if (userId != null) {
|
||||
predicates.add(cb.equal(root.get("user").get("id"), userId));
|
||||
}
|
||||
|
||||
if (search != null && !search.trim().isEmpty()) {
|
||||
String searchPattern = "%" + search.trim() + "%";
|
||||
predicates.add(cb.or(
|
||||
cb.like(cb.lower(root.get("subject")), searchPattern.toLowerCase())
|
||||
));
|
||||
}
|
||||
|
||||
return cb.and(predicates.toArray(new Predicate[0]));
|
||||
};
|
||||
|
||||
Page<SupportTicket> ticketPage = supportTicketRepository.findAll(spec, pageable);
|
||||
|
||||
// Fetch user names and message counts
|
||||
List<Integer> userIds = ticketPage.getContent().stream()
|
||||
.map(t -> t.getUser().getId())
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Map<Integer, String> userNameMap = userARepository.findAllById(userIds).stream()
|
||||
.collect(Collectors.toMap(
|
||||
UserA::getId,
|
||||
u -> u.getTelegramName() != null && !u.getTelegramName().equals("-")
|
||||
? u.getTelegramName()
|
||||
: u.getScreenName()
|
||||
));
|
||||
|
||||
// Convert to DTOs
|
||||
Page<AdminSupportTicketDto> dtoPage = ticketPage.map(ticket -> {
|
||||
String userName = userNameMap.getOrDefault(ticket.getUser().getId(), "Unknown");
|
||||
long messageCount = supportMessageRepository.countByTicketId(ticket.getId());
|
||||
|
||||
// Get last message preview
|
||||
List<SupportMessage> lastMessages = supportMessageRepository.findByTicketIdOrderByCreatedAtAsc(ticket.getId());
|
||||
String lastMessagePreview = "";
|
||||
Instant lastMessageAt = null;
|
||||
if (!lastMessages.isEmpty()) {
|
||||
SupportMessage lastMsg = lastMessages.get(lastMessages.size() - 1);
|
||||
lastMessagePreview = lastMsg.getMessage().length() > 100
|
||||
? lastMsg.getMessage().substring(0, 100) + "..."
|
||||
: lastMsg.getMessage();
|
||||
lastMessageAt = lastMsg.getCreatedAt();
|
||||
}
|
||||
|
||||
return AdminSupportTicketDto.builder()
|
||||
.id(ticket.getId())
|
||||
.userId(ticket.getUser().getId())
|
||||
.userName(userName)
|
||||
.subject(ticket.getSubject())
|
||||
.status(ticket.getStatus().name())
|
||||
.createdAt(ticket.getCreatedAt())
|
||||
.updatedAt(ticket.getUpdatedAt())
|
||||
.messageCount(messageCount)
|
||||
.lastMessagePreview(lastMessagePreview)
|
||||
.lastMessageAt(lastMessageAt)
|
||||
.build();
|
||||
});
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("content", dtoPage.getContent());
|
||||
response.put("totalElements", dtoPage.getTotalElements());
|
||||
response.put("totalPages", dtoPage.getTotalPages());
|
||||
response.put("currentPage", dtoPage.getNumber());
|
||||
response.put("size", dtoPage.getSize());
|
||||
response.put("hasNext", dtoPage.hasNext());
|
||||
response.put("hasPrevious", dtoPage.hasPrevious());
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<AdminSupportTicketDetailDto> getTicketDetail(@PathVariable Long id) {
|
||||
return supportTicketRepository.findById(id)
|
||||
.map(ticket -> {
|
||||
String userName = ticket.getUser().getTelegramName() != null && !ticket.getUser().getTelegramName().equals("-")
|
||||
? ticket.getUser().getTelegramName()
|
||||
: ticket.getUser().getScreenName();
|
||||
|
||||
List<SupportMessage> messages = supportMessageRepository.findByTicketIdOrderByCreatedAtAsc(id);
|
||||
|
||||
// Get all admin user IDs from admins table
|
||||
List<Integer> adminUserIds = adminRepository.findAll().stream()
|
||||
.filter(admin -> admin.getUserId() != null)
|
||||
.map(Admin::getUserId)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<AdminSupportMessageDto> messageDtos = messages.stream()
|
||||
.map(msg -> {
|
||||
// Check if message is from admin by checking if user_id is in admins table
|
||||
boolean isAdmin = adminUserIds.contains(msg.getUser().getId());
|
||||
String msgUserName = msg.getUser().getTelegramName() != null && !msg.getUser().getTelegramName().equals("-")
|
||||
? msg.getUser().getTelegramName()
|
||||
: msg.getUser().getScreenName();
|
||||
|
||||
return AdminSupportMessageDto.builder()
|
||||
.id(msg.getId())
|
||||
.userId(msg.getUser().getId())
|
||||
.userName(msgUserName)
|
||||
.message(msg.getMessage())
|
||||
.createdAt(msg.getCreatedAt())
|
||||
.isAdmin(isAdmin)
|
||||
.build();
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return AdminSupportTicketDetailDto.builder()
|
||||
.id(ticket.getId())
|
||||
.userId(ticket.getUser().getId())
|
||||
.userName(userName)
|
||||
.subject(ticket.getSubject())
|
||||
.status(ticket.getStatus().name())
|
||||
.createdAt(ticket.getCreatedAt())
|
||||
.updatedAt(ticket.getUpdatedAt())
|
||||
.messages(messageDtos)
|
||||
.build();
|
||||
})
|
||||
.map(ResponseEntity::ok)
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/reply")
|
||||
public ResponseEntity<?> replyToTicket(
|
||||
@PathVariable Long id,
|
||||
@Valid @RequestBody SupportTicketReplyRequest request) {
|
||||
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
String adminUsername;
|
||||
|
||||
if (authentication.getPrincipal() instanceof UserDetails) {
|
||||
adminUsername = ((UserDetails) authentication.getPrincipal()).getUsername();
|
||||
} else {
|
||||
// Fallback if principal is a String
|
||||
adminUsername = authentication.getName();
|
||||
}
|
||||
|
||||
// Get admin entity to find user_id
|
||||
Admin admin = adminRepository.findByUsername(adminUsername)
|
||||
.orElseThrow(() -> new RuntimeException("Admin not found: " + adminUsername));
|
||||
|
||||
if (admin.getUserId() == null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", "Admin account is not linked to a user account"));
|
||||
}
|
||||
|
||||
// Get the admin's UserA entity
|
||||
UserA adminUser = userARepository.findById(admin.getUserId())
|
||||
.orElseThrow(() -> new RuntimeException("Admin user account not found: " + admin.getUserId()));
|
||||
|
||||
return supportTicketRepository.findById(id)
|
||||
.map(ticket -> {
|
||||
// Create message without prefix, using admin's user_id
|
||||
SupportMessage message = SupportMessage.builder()
|
||||
.ticket(ticket)
|
||||
.user(adminUser) // Use admin's UserA entity
|
||||
.message(request.getMessage()) // Save message as-is, no prefix
|
||||
.build();
|
||||
|
||||
supportMessageRepository.save(message);
|
||||
|
||||
// Update ticket updated_at
|
||||
ticket.setUpdatedAt(java.time.Instant.now());
|
||||
if (ticket.getStatus() == SupportTicket.TicketStatus.CLOSED) {
|
||||
ticket.setStatus(SupportTicket.TicketStatus.OPENED); // Reopen if admin replies
|
||||
}
|
||||
supportTicketRepository.save(ticket);
|
||||
|
||||
return ResponseEntity.ok(Map.of("message", "Reply sent successfully"));
|
||||
})
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/close")
|
||||
public ResponseEntity<?> closeTicket(@PathVariable Long id) {
|
||||
return supportTicketRepository.findById(id)
|
||||
.map(ticket -> {
|
||||
ticket.setStatus(SupportTicket.TicketStatus.CLOSED);
|
||||
ticket.setUpdatedAt(java.time.Instant.now());
|
||||
supportTicketRepository.save(ticket);
|
||||
return ResponseEntity.ok(Map.of("message", "Ticket closed"));
|
||||
})
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/reopen")
|
||||
public ResponseEntity<?> reopenTicket(@PathVariable Long id) {
|
||||
return supportTicketRepository.findById(id)
|
||||
.map(ticket -> {
|
||||
ticket.setStatus(SupportTicket.TicketStatus.OPENED);
|
||||
ticket.setUpdatedAt(java.time.Instant.now());
|
||||
supportTicketRepository.save(ticket);
|
||||
return ResponseEntity.ok(Map.of("message", "Ticket reopened"));
|
||||
})
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@PutMapping("/messages/{messageId}")
|
||||
public ResponseEntity<?> editMessage(
|
||||
@PathVariable Long messageId,
|
||||
@Valid @RequestBody SupportTicketReplyRequest request) {
|
||||
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
String adminUsername;
|
||||
|
||||
if (authentication.getPrincipal() instanceof UserDetails) {
|
||||
adminUsername = ((UserDetails) authentication.getPrincipal()).getUsername();
|
||||
} else {
|
||||
adminUsername = authentication.getName();
|
||||
}
|
||||
|
||||
// Get admin entity to find user_id
|
||||
Admin admin = adminRepository.findByUsername(adminUsername)
|
||||
.orElseThrow(() -> new RuntimeException("Admin not found: " + adminUsername));
|
||||
|
||||
if (admin.getUserId() == null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", "Admin account is not linked to a user account"));
|
||||
}
|
||||
|
||||
return supportMessageRepository.findById(messageId)
|
||||
.map(message -> {
|
||||
// Check if message is from this admin
|
||||
if (!message.getUser().getId().equals(admin.getUserId())) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", "You can only edit your own messages"));
|
||||
}
|
||||
|
||||
// Check if user is an admin (verify in admins table)
|
||||
boolean isAdmin = adminRepository.findAll().stream()
|
||||
.anyMatch(a -> a.getUserId() != null && a.getUserId().equals(message.getUser().getId()));
|
||||
|
||||
if (!isAdmin) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", "Only admin messages can be edited"));
|
||||
}
|
||||
|
||||
// Update message
|
||||
message.setMessage(request.getMessage());
|
||||
supportMessageRepository.save(message);
|
||||
|
||||
// Update ticket updated_at
|
||||
message.getTicket().setUpdatedAt(java.time.Instant.now());
|
||||
supportTicketRepository.save(message.getTicket());
|
||||
|
||||
return ResponseEntity.ok(Map.of("message", "Message updated successfully"));
|
||||
})
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
package com.honey.honey.controller;
|
||||
|
||||
import com.honey.honey.dto.*;
|
||||
import com.honey.honey.service.AdminUserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/users")
|
||||
@RequiredArgsConstructor
|
||||
public class AdminUserController {
|
||||
|
||||
/** Sortable fields: UserA properties plus UserB/UserD (handled via custom query in service). */
|
||||
private static final Set<String> SORTABLE_FIELDS = Set.of(
|
||||
"id", "screenName", "telegramId", "telegramName", "isPremium",
|
||||
"languageCode", "countryCode", "deviceCode", "dateReg", "dateLogin", "banned",
|
||||
"balanceA", "depositTotal", "withdrawTotal", "referralCount", "profit"
|
||||
);
|
||||
private static final Set<String> DEPOSIT_SORT_FIELDS = Set.of("id", "usdAmount", "status", "orderId", "createdAt", "completedAt");
|
||||
private static final Set<String> WITHDRAWAL_SORT_FIELDS = Set.of("id", "usdAmount", "cryptoName", "amountToSend", "txhash", "status", "paymentId", "createdAt", "resolvedAt");
|
||||
|
||||
private final AdminUserService adminUserService;
|
||||
|
||||
private boolean isGameAdmin() {
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (auth == null || auth.getAuthorities() == null) return false;
|
||||
return auth.getAuthorities().stream()
|
||||
.anyMatch(a -> "ROLE_GAME_ADMIN".equals(a.getAuthority()));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAnyRole('ADMIN', 'GAME_ADMIN')")
|
||||
public ResponseEntity<Map<String, Object>> getUsers(
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "50") int size,
|
||||
@RequestParam(required = false) String sortBy,
|
||||
@RequestParam(required = false) String sortDir,
|
||||
// Filters
|
||||
@RequestParam(required = false) String search,
|
||||
@RequestParam(required = false) Integer banned,
|
||||
@RequestParam(required = false) String countryCode,
|
||||
@RequestParam(required = false) String languageCode,
|
||||
@RequestParam(required = false) Integer dateRegFrom,
|
||||
@RequestParam(required = false) Integer dateRegTo,
|
||||
@RequestParam(required = false) Long balanceMin,
|
||||
@RequestParam(required = false) Long balanceMax,
|
||||
@RequestParam(required = false) Integer referralCountMin,
|
||||
@RequestParam(required = false) Integer referralCountMax,
|
||||
@RequestParam(required = false) Integer referrerId,
|
||||
@RequestParam(required = false) Integer referralLevel,
|
||||
@RequestParam(required = false) String ip) {
|
||||
|
||||
// Build sort. Fields on UserB/UserD (balanceA, depositTotal, withdrawTotal, referralCount) are handled in service via custom query.
|
||||
Set<String> sortRequiresJoin = Set.of("balanceA", "depositTotal", "withdrawTotal", "referralCount", "profit");
|
||||
String effectiveSortBy = sortBy != null && sortBy.trim().isEmpty() ? null : (sortBy != null ? sortBy.trim() : null);
|
||||
if (effectiveSortBy != null && sortRequiresJoin.contains(effectiveSortBy)) {
|
||||
// Pass through; service will use custom ordered query
|
||||
} else if (effectiveSortBy != null && !SORTABLE_FIELDS.contains(effectiveSortBy)) {
|
||||
effectiveSortBy = null;
|
||||
}
|
||||
Sort sort = Sort.by("id").descending();
|
||||
if (effectiveSortBy != null && !sortRequiresJoin.contains(effectiveSortBy)) {
|
||||
Sort.Direction direction = "asc".equalsIgnoreCase(sortDir) ? Sort.Direction.ASC : Sort.Direction.DESC;
|
||||
sort = Sort.by(direction, effectiveSortBy);
|
||||
}
|
||||
Pageable pageable = PageRequest.of(page, size, sort);
|
||||
|
||||
// Convert balance filters from tickets (divide by 1000000) to bigint format
|
||||
Long balanceMinBigint = balanceMin != null ? balanceMin * 1000000L : null;
|
||||
Long balanceMaxBigint = balanceMax != null ? balanceMax * 1000000L : null;
|
||||
|
||||
boolean excludeMasters = isGameAdmin();
|
||||
Page<AdminUserDto> dtoPage = adminUserService.getUsers(
|
||||
pageable,
|
||||
search,
|
||||
banned,
|
||||
countryCode,
|
||||
languageCode,
|
||||
dateRegFrom,
|
||||
dateRegTo,
|
||||
balanceMinBigint,
|
||||
balanceMaxBigint,
|
||||
referralCountMin,
|
||||
referralCountMax,
|
||||
referrerId,
|
||||
referralLevel,
|
||||
ip,
|
||||
effectiveSortBy,
|
||||
sortDir,
|
||||
excludeMasters
|
||||
);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("content", dtoPage.getContent());
|
||||
response.put("totalElements", dtoPage.getTotalElements());
|
||||
response.put("totalPages", dtoPage.getTotalPages());
|
||||
response.put("currentPage", dtoPage.getNumber());
|
||||
response.put("size", dtoPage.getSize());
|
||||
response.put("hasNext", dtoPage.hasNext());
|
||||
response.put("hasPrevious", dtoPage.hasPrevious());
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasAnyRole('ADMIN', 'GAME_ADMIN')")
|
||||
public ResponseEntity<AdminUserDetailDto> getUserDetail(@PathVariable Integer id) {
|
||||
AdminUserDetailDto userDetail = adminUserService.getUserDetail(id, isGameAdmin());
|
||||
if (userDetail == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(userDetail);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/transactions")
|
||||
@PreAuthorize("hasAnyRole('ADMIN', 'GAME_ADMIN')")
|
||||
public ResponseEntity<Map<String, Object>> getUserTransactions(
|
||||
@PathVariable Integer id,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "50") int size) {
|
||||
|
||||
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
|
||||
Page<AdminTransactionDto> transactions = adminUserService.getUserTransactions(id, pageable);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("content", transactions.getContent());
|
||||
response.put("totalElements", transactions.getTotalElements());
|
||||
response.put("totalPages", transactions.getTotalPages());
|
||||
response.put("currentPage", transactions.getNumber());
|
||||
response.put("size", transactions.getSize());
|
||||
response.put("hasNext", transactions.hasNext());
|
||||
response.put("hasPrevious", transactions.hasPrevious());
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/payments")
|
||||
@PreAuthorize("hasAnyRole('ADMIN', 'GAME_ADMIN')")
|
||||
public ResponseEntity<Map<String, Object>> getUserPayments(
|
||||
@PathVariable Integer id,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "20") int size,
|
||||
@RequestParam(required = false) String sortBy,
|
||||
@RequestParam(required = false) String sortDir) {
|
||||
String field = sortBy != null && DEPOSIT_SORT_FIELDS.contains(sortBy.trim()) ? sortBy.trim() : "createdAt";
|
||||
Sort.Direction dir = "asc".equalsIgnoreCase(sortDir) ? Sort.Direction.ASC : Sort.Direction.DESC;
|
||||
Pageable pageable = PageRequest.of(page, size, Sort.by(dir, field));
|
||||
Page<UserDepositDto> deposits = adminUserService.getUserPayments(id, pageable);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("content", deposits.getContent());
|
||||
response.put("totalElements", deposits.getTotalElements());
|
||||
response.put("totalPages", deposits.getTotalPages());
|
||||
response.put("currentPage", deposits.getNumber());
|
||||
response.put("size", deposits.getSize());
|
||||
response.put("hasNext", deposits.hasNext());
|
||||
response.put("hasPrevious", deposits.hasPrevious());
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/payouts")
|
||||
@PreAuthorize("hasAnyRole('ADMIN', 'GAME_ADMIN')")
|
||||
public ResponseEntity<Map<String, Object>> getUserPayouts(
|
||||
@PathVariable Integer id,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "20") int size,
|
||||
@RequestParam(required = false) String sortBy,
|
||||
@RequestParam(required = false) String sortDir) {
|
||||
String field = sortBy != null && WITHDRAWAL_SORT_FIELDS.contains(sortBy.trim()) ? sortBy.trim() : "createdAt";
|
||||
Sort.Direction dir = "asc".equalsIgnoreCase(sortDir) ? Sort.Direction.ASC : Sort.Direction.DESC;
|
||||
Pageable pageable = PageRequest.of(page, size, Sort.by(dir, field));
|
||||
Page<UserWithdrawalDto> payouts = adminUserService.getUserPayouts(id, pageable);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("content", payouts.getContent());
|
||||
response.put("totalElements", payouts.getTotalElements());
|
||||
response.put("totalPages", payouts.getTotalPages());
|
||||
response.put("currentPage", payouts.getNumber());
|
||||
response.put("size", payouts.getSize());
|
||||
response.put("hasNext", payouts.hasNext());
|
||||
response.put("hasPrevious", payouts.hasPrevious());
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/tasks")
|
||||
@PreAuthorize("hasAnyRole('ADMIN', 'GAME_ADMIN')")
|
||||
public ResponseEntity<Map<String, Object>> getUserTasks(@PathVariable Integer id) {
|
||||
Map<String, Object> tasks = adminUserService.getUserTasks(id);
|
||||
return ResponseEntity.ok(tasks);
|
||||
}
|
||||
|
||||
@PatchMapping("/{id}/ban")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public ResponseEntity<?> setUserBanned(
|
||||
@PathVariable Integer id,
|
||||
@RequestBody Map<String, Boolean> body) {
|
||||
Boolean banned = body != null ? body.get("banned") : null;
|
||||
if (banned == null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", "banned is required (true/false)"));
|
||||
}
|
||||
try {
|
||||
adminUserService.setBanned(id, banned);
|
||||
return ResponseEntity.ok().build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
|
||||
@PatchMapping("/{id}/withdrawals-enabled")
|
||||
@PreAuthorize("hasAnyRole('ADMIN', 'GAME_ADMIN')")
|
||||
public ResponseEntity<?> setWithdrawalsEnabled(
|
||||
@PathVariable Integer id,
|
||||
@RequestBody Map<String, Boolean> body) {
|
||||
Boolean enabled = body != null ? body.get("enabled") : null;
|
||||
if (enabled == null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", "enabled is required (true/false)"));
|
||||
}
|
||||
try {
|
||||
adminUserService.setWithdrawalsEnabled(id, enabled);
|
||||
return ResponseEntity.ok().build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/balance/adjust")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public ResponseEntity<?> adjustBalance(
|
||||
@PathVariable Integer id,
|
||||
@Valid @RequestBody com.honey.honey.dto.BalanceAdjustmentRequest request) {
|
||||
try {
|
||||
com.honey.honey.dto.BalanceAdjustmentResponse response = adminUserService.adjustBalance(id, request);
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
99
src/main/java/com/honey/honey/controller/AuthController.java
Normal file
99
src/main/java/com/honey/honey/controller/AuthController.java
Normal file
@@ -0,0 +1,99 @@
|
||||
package com.honey.honey.controller;
|
||||
|
||||
import com.honey.honey.dto.CreateSessionRequest;
|
||||
import com.honey.honey.dto.CreateSessionResponse;
|
||||
import com.honey.honey.exception.BannedUserException;
|
||||
import com.honey.honey.model.UserA;
|
||||
import com.honey.honey.service.LocalizationService;
|
||||
import com.honey.honey.service.SessionService;
|
||||
import com.honey.honey.service.TelegramAuthService;
|
||||
import com.honey.honey.service.UserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
@RequiredArgsConstructor
|
||||
public class AuthController {
|
||||
|
||||
private final TelegramAuthService telegramAuthService;
|
||||
private final SessionService sessionService;
|
||||
private final UserService userService;
|
||||
private final LocalizationService localizationService;
|
||||
|
||||
/**
|
||||
* Creates a session by validating Telegram initData.
|
||||
* This is the only endpoint that accepts initData.
|
||||
* Handles user registration/login and referral system.
|
||||
*/
|
||||
@PostMapping("/tma/session")
|
||||
public CreateSessionResponse createSession(
|
||||
@RequestBody CreateSessionRequest request,
|
||||
HttpServletRequest httpRequest) {
|
||||
String initData = request.getInitData();
|
||||
|
||||
if (initData == null || initData.isBlank()) {
|
||||
throw new IllegalArgumentException(localizationService.getMessage("auth.error.initDataRequired"));
|
||||
}
|
||||
|
||||
// Validate Telegram initData signature and parse data
|
||||
Map<String, Object> tgUserData = telegramAuthService.validateAndParseInitData(initData);
|
||||
|
||||
// Get or create user (handles registration, login update, and referral system)
|
||||
// Note: Referral handling is done via bot registration endpoint, not through WebApp initData
|
||||
UserA user = userService.getOrCreateUser(tgUserData, httpRequest);
|
||||
|
||||
if (user.getBanned() != null && user.getBanned() == 1) {
|
||||
String message = localizationService.getMessageForUser(user.getId(), "auth.error.accessRestricted");
|
||||
throw new BannedUserException(message);
|
||||
}
|
||||
|
||||
// Create session
|
||||
String sessionId = sessionService.createSession(user);
|
||||
|
||||
log.debug("Session created: userId={}", user.getId());
|
||||
|
||||
return CreateSessionResponse.builder()
|
||||
.access_token(sessionId)
|
||||
.expires_in(sessionService.getSessionTtlSeconds())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs out by invalidating the session.
|
||||
* This endpoint requires authentication (Bearer token).
|
||||
*/
|
||||
@PostMapping("/logout")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
public void logout(@RequestHeader(value = "Authorization", required = false) String authHeader) {
|
||||
if (authHeader == null) {
|
||||
log.warn("Logout called without Authorization header");
|
||||
return;
|
||||
}
|
||||
|
||||
String sessionId = extractBearerToken(authHeader);
|
||||
if (sessionId != null) {
|
||||
sessionService.invalidateSession(sessionId);
|
||||
log.debug("Session invalidated via logout");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts Bearer token from Authorization header.
|
||||
*/
|
||||
private String extractBearerToken(String authHeader) {
|
||||
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
||||
return authHeader.substring(7);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.honey.honey.controller;
|
||||
|
||||
import com.honey.honey.dto.ExternalDepositWebhookRequest;
|
||||
import com.honey.honey.service.PaymentService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* Controller for 3rd party deposit completion webhook.
|
||||
* Path: POST /api/deposit_webhook/{token}. Token must match app.deposit-webhook.token (APP_DEPOSIT_WEBHOOK_TOKEN).
|
||||
* No session auth; token in path only. Set the token on VPS via environment variable.
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/deposit_webhook")
|
||||
@RequiredArgsConstructor
|
||||
public class DepositWebhookController {
|
||||
|
||||
@Value("${app.deposit-webhook.token:}")
|
||||
private String expectedToken;
|
||||
|
||||
private final PaymentService paymentService;
|
||||
|
||||
/**
|
||||
* Called by 3rd party when a user's crypto deposit was successful.
|
||||
* Body: user_id (internal id from db_users_a), usd_amount (decimal, e.g. 1.45).
|
||||
*/
|
||||
@PostMapping("/{token}")
|
||||
public ResponseEntity<Void> onDepositCompleted(
|
||||
@PathVariable String token,
|
||||
@RequestBody ExternalDepositWebhookRequest request) {
|
||||
if (expectedToken.isEmpty() || !expectedToken.equals(token)) {
|
||||
log.warn("Deposit webhook rejected: invalid token");
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||
}
|
||||
if (request == null || request.getUserId() == null || request.getUsdAmount() == null) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
try {
|
||||
paymentService.processExternalDepositCompletion(
|
||||
request.getUserId(),
|
||||
request.getUsdAmount());
|
||||
return ResponseEntity.ok().build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("Deposit webhook rejected: {}", e.getMessage());
|
||||
return ResponseEntity.badRequest().build();
|
||||
} catch (Exception e) {
|
||||
log.error("Deposit webhook error: userId={}, usdAmount={}", request.getUserId(), request.getUsdAmount(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.honey.honey.controller;
|
||||
|
||||
import com.honey.honey.dto.NotifyBroadcastRequest;
|
||||
import com.honey.honey.service.NotificationBroadcastService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* Public API to trigger or stop notification broadcast. Token in path; no secrets in codebase.
|
||||
* Set APP_NOTIFY_BROADCAST_TOKEN on VPS.
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/notify_broadcast")
|
||||
@RequiredArgsConstructor
|
||||
public class NotifyBroadcastController {
|
||||
|
||||
@Value("${app.notify-broadcast.token:}")
|
||||
private String expectedToken;
|
||||
|
||||
private final NotificationBroadcastService notificationBroadcastService;
|
||||
|
||||
@PostMapping("/{token}")
|
||||
public ResponseEntity<Void> start(
|
||||
@PathVariable String token,
|
||||
@RequestBody(required = false) NotifyBroadcastRequest body) {
|
||||
if (expectedToken.isEmpty() || !expectedToken.equals(token)) {
|
||||
log.warn("Notify broadcast rejected: invalid token");
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||
}
|
||||
NotifyBroadcastRequest req = body != null ? body : new NotifyBroadcastRequest();
|
||||
notificationBroadcastService.runBroadcast(
|
||||
req.getMessage(),
|
||||
req.getImageUrl(),
|
||||
req.getVideoUrl(),
|
||||
req.getUserIdFrom(),
|
||||
req.getUserIdTo(),
|
||||
req.getButtonText(),
|
||||
req.getIgnoreBlocked());
|
||||
return ResponseEntity.accepted().build();
|
||||
}
|
||||
|
||||
@PostMapping("/{token}/stop")
|
||||
public ResponseEntity<Void> stop(@PathVariable String token) {
|
||||
if (expectedToken.isEmpty() || !expectedToken.equals(token)) {
|
||||
log.warn("Notify broadcast stop rejected: invalid token");
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||
}
|
||||
notificationBroadcastService.requestStop();
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
}
|
||||
237
src/main/java/com/honey/honey/controller/PaymentController.java
Normal file
237
src/main/java/com/honey/honey/controller/PaymentController.java
Normal file
@@ -0,0 +1,237 @@
|
||||
package com.honey.honey.controller;
|
||||
|
||||
import com.honey.honey.dto.CryptoWithdrawalResponse;
|
||||
import com.honey.honey.dto.CreateCryptoWithdrawalRequest;
|
||||
import com.honey.honey.dto.CreatePaymentRequest;
|
||||
import com.honey.honey.dto.DepositAddressRequest;
|
||||
import com.honey.honey.dto.DepositAddressResultDto;
|
||||
import com.honey.honey.dto.DepositMethodsDto;
|
||||
import com.honey.honey.dto.ErrorResponse;
|
||||
import com.honey.honey.dto.PaymentInvoiceResponse;
|
||||
import com.honey.honey.model.Payout;
|
||||
import com.honey.honey.model.UserA;
|
||||
import com.honey.honey.security.UserContext;
|
||||
import com.honey.honey.dto.WithdrawalMethodDetailsDto;
|
||||
import com.honey.honey.dto.WithdrawalMethodsDto;
|
||||
import com.honey.honey.service.CryptoDepositService;
|
||||
import com.honey.honey.service.CryptoWithdrawalService;
|
||||
import com.honey.honey.service.FeatureSwitchService;
|
||||
import com.honey.honey.service.LocalizationService;
|
||||
import com.honey.honey.service.PaymentService;
|
||||
import com.honey.honey.service.PayoutService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/payments")
|
||||
@RequiredArgsConstructor
|
||||
public class PaymentController {
|
||||
|
||||
private final PaymentService paymentService;
|
||||
private final CryptoDepositService cryptoDepositService;
|
||||
private final CryptoWithdrawalService cryptoWithdrawalService;
|
||||
private final PayoutService payoutService;
|
||||
private final FeatureSwitchService featureSwitchService;
|
||||
private final LocalizationService localizationService;
|
||||
|
||||
/**
|
||||
* Returns minimum deposit from DB only (no sync). Used by Store screen for validation.
|
||||
*/
|
||||
@GetMapping("/minimum-deposit")
|
||||
public ResponseEntity<?> getMinimumDeposit() {
|
||||
if (!featureSwitchService.isPaymentEnabled()) {
|
||||
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
|
||||
.body(new ErrorResponse(localizationService.getMessage("feature.depositsUnavailable")));
|
||||
}
|
||||
return ResponseEntity.ok(java.util.Map.of("minimumDeposit", cryptoDepositService.getMinimumDeposit()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns crypto deposit methods and minimum_deposit from DB only (sync is done every 10 min).
|
||||
* Called when user opens Payment Options screen.
|
||||
*/
|
||||
@GetMapping("/deposit-methods")
|
||||
public ResponseEntity<?> getDepositMethods() {
|
||||
if (!featureSwitchService.isPaymentEnabled()) {
|
||||
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
|
||||
.body(new ErrorResponse(localizationService.getMessage("feature.depositsUnavailable")));
|
||||
}
|
||||
DepositMethodsDto dto = cryptoDepositService.getDepositMethodsFromDb();
|
||||
return ResponseEntity.ok(dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns crypto withdrawal methods from DB only (sync is done every 30 min).
|
||||
* Called when user opens Payout screen.
|
||||
*/
|
||||
@GetMapping("/withdrawal-methods")
|
||||
public ResponseEntity<?> getWithdrawalMethods() {
|
||||
if (!featureSwitchService.isPayoutEnabled()) {
|
||||
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
|
||||
.body(new ErrorResponse(localizationService.getMessage("feature.payoutsUnavailable")));
|
||||
}
|
||||
WithdrawalMethodsDto dto = cryptoWithdrawalService.getWithdrawalMethodsFromDb();
|
||||
return ResponseEntity.ok(dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns withdrawal method details (rate_usd, misha_fee_usd) from external API for the given pid.
|
||||
* Called when user opens Payout Confirmation screen to show network fee and compute "You will receive".
|
||||
*/
|
||||
@GetMapping("/withdrawal-method-details")
|
||||
public ResponseEntity<?> getWithdrawalMethodDetails(@RequestParam("pid") int pid) {
|
||||
if (!featureSwitchService.isPayoutEnabled()) {
|
||||
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
|
||||
.body(new ErrorResponse(localizationService.getMessage("feature.payoutsUnavailable")));
|
||||
}
|
||||
return cryptoWithdrawalService.getWithdrawalMethodDetails(pid)
|
||||
.map(ResponseEntity::ok)
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a crypto withdrawal: calls external API, then on success creates payout and deducts balance.
|
||||
* Uses in-memory lock to prevent double-submit. Validates deposit total and maxWinAfterDeposit.
|
||||
*/
|
||||
@PostMapping("/crypto-withdrawal")
|
||||
public ResponseEntity<?> createCryptoWithdrawal(@RequestBody CreateCryptoWithdrawalRequest request) {
|
||||
if (!featureSwitchService.isPayoutEnabled()) {
|
||||
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
|
||||
.body(new ErrorResponse(localizationService.getMessage("feature.payoutsUnavailable")));
|
||||
}
|
||||
try {
|
||||
UserA user = UserContext.get();
|
||||
if (user == null) {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorResponse("Authentication required"));
|
||||
}
|
||||
Payout payout = payoutService.createCryptoPayout(user.getId(), request);
|
||||
return ResponseEntity.ok(CryptoWithdrawalResponse.builder()
|
||||
.id(payout.getId())
|
||||
.status(payout.getStatus().name())
|
||||
.build());
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("Crypto withdrawal validation failed: {}", e.getMessage());
|
||||
return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage()));
|
||||
} catch (IllegalStateException e) {
|
||||
log.warn("Crypto withdrawal failed: {}", e.getMessage());
|
||||
return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage()));
|
||||
} catch (Exception e) {
|
||||
log.error("Crypto withdrawal error: {}", e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(new ErrorResponse("Withdrawal failed. Please try again later."));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a payment invoice for the current user.
|
||||
* Returns invoice data that frontend will use to open Telegram payment UI.
|
||||
*/
|
||||
@PostMapping("/create")
|
||||
public ResponseEntity<?> createPaymentInvoice(@RequestBody CreatePaymentRequest request) {
|
||||
if (!featureSwitchService.isPaymentEnabled()) {
|
||||
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
|
||||
.body(new ErrorResponse(localizationService.getMessage("feature.depositsUnavailable")));
|
||||
}
|
||||
try {
|
||||
UserA user = UserContext.get();
|
||||
PaymentInvoiceResponse response = paymentService.createPaymentInvoice(user.getId(), request);
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("Payment invoice creation failed: {}", e.getMessage());
|
||||
return ResponseEntity.badRequest()
|
||||
.body(new ErrorResponse(e.getMessage()));
|
||||
} catch (Exception e) {
|
||||
log.error("Payment invoice creation error: {}", e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(new ErrorResponse("Failed to create payment invoice: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets a crypto deposit address from the external API (no payment record is created).
|
||||
* Call when user selects a payment method on Payment Options screen.
|
||||
* Returns address, amount_coins, name, network for the Payment Confirmation screen.
|
||||
*/
|
||||
@PostMapping("/deposit-address")
|
||||
public ResponseEntity<?> getDepositAddress(@RequestBody DepositAddressRequest request) {
|
||||
if (!featureSwitchService.isPaymentEnabled()) {
|
||||
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
|
||||
.body(new ErrorResponse(localizationService.getMessage("feature.depositsUnavailable")));
|
||||
}
|
||||
try {
|
||||
UserA user = UserContext.get();
|
||||
if (user == null) {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorResponse("Authentication required"));
|
||||
}
|
||||
DepositAddressResultDto result = paymentService.requestCryptoDepositAddress(
|
||||
user.getId(), request.getPid(), request.getUsdAmount());
|
||||
return ResponseEntity.ok(result);
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("Deposit address request failed: {}", e.getMessage());
|
||||
return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage()));
|
||||
} catch (Exception e) {
|
||||
log.error("Deposit address error: {}", e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(new ErrorResponse(e.getMessage() != null ? e.getMessage() : "Failed to get deposit address"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels a payment (e.g., when user cancels in Telegram UI).
|
||||
*/
|
||||
@PostMapping("/cancel")
|
||||
public ResponseEntity<?> cancelPayment(@RequestBody CancelPaymentRequest request) {
|
||||
try {
|
||||
String orderId = request.getOrderId();
|
||||
UserA caller = UserContext.get();
|
||||
log.info("Payment cancel requested: orderId={}, callerUserId={}", orderId, caller != null ? caller.getId() : null);
|
||||
paymentService.cancelPayment(orderId);
|
||||
return ResponseEntity.ok().body(new PaymentWebhookResponse(true, "Payment cancelled"));
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("Payment cancellation failed: {}", e.getMessage());
|
||||
return ResponseEntity.badRequest()
|
||||
.body(new ErrorResponse(e.getMessage()));
|
||||
} catch (Exception e) {
|
||||
log.error("Payment cancellation error: {}", e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(new ErrorResponse("Failed to cancel payment: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
// Response DTOs
|
||||
private static class PaymentWebhookResponse {
|
||||
private final boolean success;
|
||||
private final String message;
|
||||
|
||||
public PaymentWebhookResponse(boolean success, String message) {
|
||||
this.success = success;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public boolean isSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
private static class CancelPaymentRequest {
|
||||
private String orderId;
|
||||
|
||||
public String getOrderId() {
|
||||
return orderId;
|
||||
}
|
||||
|
||||
public void setOrderId(String orderId) {
|
||||
this.orderId = orderId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.honey.honey.controller;
|
||||
|
||||
import com.honey.honey.dto.CreatePayoutRequest;
|
||||
import com.honey.honey.dto.ErrorResponse;
|
||||
import com.honey.honey.dto.PayoutHistoryEntryDto;
|
||||
import com.honey.honey.dto.PayoutResponse;
|
||||
import com.honey.honey.model.Payout;
|
||||
import com.honey.honey.model.UserA;
|
||||
import com.honey.honey.security.UserContext;
|
||||
import com.honey.honey.service.PayoutService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/payouts")
|
||||
@RequiredArgsConstructor
|
||||
public class PayoutController {
|
||||
|
||||
private final PayoutService payoutService;
|
||||
|
||||
/**
|
||||
* Creates a payout request for the current user.
|
||||
* Validates input and deducts balance if validation passes.
|
||||
*/
|
||||
@PostMapping
|
||||
public ResponseEntity<?> createPayout(@RequestBody CreatePayoutRequest request) {
|
||||
try {
|
||||
UserA user = UserContext.get();
|
||||
Payout payout = payoutService.createPayout(user.getId(), request);
|
||||
PayoutResponse response = payoutService.toResponse(payout);
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("Payout validation failed: {}", e.getMessage());
|
||||
return ResponseEntity.badRequest()
|
||||
.body(new ErrorResponse(e.getMessage()));
|
||||
} catch (IllegalStateException e) {
|
||||
log.error("Payout creation failed: {}", e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(new ErrorResponse(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the last 20 payout history entries for the current user.
|
||||
*
|
||||
* @param timezone Optional timezone (e.g., "Europe/London"). If not provided, uses UTC.
|
||||
*/
|
||||
@GetMapping("/history")
|
||||
public List<PayoutHistoryEntryDto> getUserPayoutHistory(
|
||||
@RequestParam(required = false) String timezone) {
|
||||
UserA user = UserContext.get();
|
||||
String languageCode = user.getLanguageCode();
|
||||
if (languageCode == null || languageCode.isEmpty() || "XX".equals(languageCode)) {
|
||||
languageCode = "EN";
|
||||
}
|
||||
return payoutService.getUserPayoutHistory(user.getId(), timezone, languageCode);
|
||||
}
|
||||
}
|
||||
|
||||
20
src/main/java/com/honey/honey/controller/PingController.java
Normal file
20
src/main/java/com/honey/honey/controller/PingController.java
Normal file
@@ -0,0 +1,20 @@
|
||||
package com.honey.honey.controller;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
public class PingController {
|
||||
|
||||
@GetMapping("/ping")
|
||||
public Map<String, String> ping() {
|
||||
Map<String, String> response = new HashMap<>();
|
||||
response.put("status", "ok");
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.honey.honey.controller;
|
||||
|
||||
import com.honey.honey.dto.PromotionDetailDto;
|
||||
import com.honey.honey.dto.PromotionListItemDto;
|
||||
import com.honey.honey.service.FeatureSwitchService;
|
||||
import com.honey.honey.service.PublicPromotionService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Public API: list and view promotion details (leaderboard, user progress).
|
||||
* Excludes INACTIVE promotions. Requires Bearer auth (app user).
|
||||
* When promotions feature switch is false, all endpoints return 404.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/promotions")
|
||||
@RequiredArgsConstructor
|
||||
public class PromotionController {
|
||||
|
||||
private final PublicPromotionService publicPromotionService;
|
||||
private final FeatureSwitchService featureSwitchService;
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<PromotionListItemDto>> list() {
|
||||
if (!featureSwitchService.isPromotionsEnabled()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(publicPromotionService.listForApp());
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<PromotionDetailDto> getDetail(@PathVariable int id) {
|
||||
if (!featureSwitchService.isPromotionsEnabled()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return publicPromotionService.getDetailForApp(id)
|
||||
.map(ResponseEntity::ok)
|
||||
.orElseGet(() -> ResponseEntity.notFound().build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package com.honey.honey.controller;
|
||||
|
||||
import com.honey.honey.dto.QuickAnswerCreateRequest;
|
||||
import com.honey.honey.dto.QuickAnswerDto;
|
||||
import com.honey.honey.model.Admin;
|
||||
import com.honey.honey.model.QuickAnswer;
|
||||
import com.honey.honey.repository.AdminRepository;
|
||||
import com.honey.honey.repository.QuickAnswerRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/quick-answers")
|
||||
@RequiredArgsConstructor
|
||||
@PreAuthorize("hasAnyRole('ADMIN', 'TICKETS_SUPPORT', 'GAME_ADMIN')")
|
||||
public class QuickAnswerController {
|
||||
|
||||
private final QuickAnswerRepository quickAnswerRepository;
|
||||
private final AdminRepository adminRepository;
|
||||
|
||||
/**
|
||||
* Get current admin from authentication context
|
||||
*/
|
||||
private Admin getCurrentAdmin() {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
String username = authentication.getName();
|
||||
return adminRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new RuntimeException("Admin not found: " + username));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all quick answers for the current admin
|
||||
*/
|
||||
@GetMapping
|
||||
public ResponseEntity<List<QuickAnswerDto>> getQuickAnswers() {
|
||||
Admin admin = getCurrentAdmin();
|
||||
List<QuickAnswer> quickAnswers = quickAnswerRepository.findByAdminIdOrderByCreatedAtDesc(admin.getId());
|
||||
List<QuickAnswerDto> dtos = quickAnswers.stream()
|
||||
.map(qa -> new QuickAnswerDto(
|
||||
qa.getId(),
|
||||
qa.getText(),
|
||||
qa.getCreatedAt(),
|
||||
qa.getUpdatedAt()
|
||||
))
|
||||
.collect(Collectors.toList());
|
||||
return ResponseEntity.ok(dtos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new quick answer for the current admin
|
||||
*/
|
||||
@PostMapping
|
||||
public ResponseEntity<QuickAnswerDto> createQuickAnswer(@Valid @RequestBody QuickAnswerCreateRequest request) {
|
||||
Admin admin = getCurrentAdmin();
|
||||
|
||||
if (request.getText() == null || request.getText().trim().isEmpty()) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
QuickAnswer quickAnswer = QuickAnswer.builder()
|
||||
.admin(admin)
|
||||
.text(request.getText().trim())
|
||||
.build();
|
||||
|
||||
QuickAnswer saved = quickAnswerRepository.save(quickAnswer);
|
||||
QuickAnswerDto dto = new QuickAnswerDto(
|
||||
saved.getId(),
|
||||
saved.getText(),
|
||||
saved.getCreatedAt(),
|
||||
saved.getUpdatedAt()
|
||||
);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a quick answer (only if it belongs to the current admin)
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<QuickAnswerDto> updateQuickAnswer(
|
||||
@PathVariable Integer id,
|
||||
@Valid @RequestBody QuickAnswerCreateRequest request) {
|
||||
Admin admin = getCurrentAdmin();
|
||||
QuickAnswer quickAnswer = quickAnswerRepository.findById(id)
|
||||
.orElseThrow(() -> new RuntimeException("Quick answer not found"));
|
||||
|
||||
// Verify that the quick answer belongs to the current admin
|
||||
if (!quickAnswer.getAdmin().getId().equals(admin.getId())) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||
}
|
||||
|
||||
if (request.getText() == null || request.getText().trim().isEmpty()) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
quickAnswer.setText(request.getText().trim());
|
||||
QuickAnswer saved = quickAnswerRepository.save(quickAnswer);
|
||||
|
||||
QuickAnswerDto dto = new QuickAnswerDto(
|
||||
saved.getId(),
|
||||
saved.getText(),
|
||||
saved.getCreatedAt(),
|
||||
saved.getUpdatedAt()
|
||||
);
|
||||
return ResponseEntity.ok(dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a quick answer (only if it belongs to the current admin)
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> deleteQuickAnswer(@PathVariable Integer id) {
|
||||
Admin admin = getCurrentAdmin();
|
||||
QuickAnswer quickAnswer = quickAnswerRepository.findById(id)
|
||||
.orElseThrow(() -> new RuntimeException("Quick answer not found"));
|
||||
|
||||
// Verify that the quick answer belongs to the current admin
|
||||
if (!quickAnswer.getAdmin().getId().equals(admin.getId())) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||
}
|
||||
|
||||
quickAnswerRepository.delete(quickAnswer);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
|
||||
104
src/main/java/com/honey/honey/controller/SupportController.java
Normal file
104
src/main/java/com/honey/honey/controller/SupportController.java
Normal file
@@ -0,0 +1,104 @@
|
||||
package com.honey.honey.controller;
|
||||
|
||||
import com.honey.honey.dto.*;
|
||||
import com.honey.honey.model.UserA;
|
||||
import com.honey.honey.security.UserContext;
|
||||
import com.honey.honey.service.SupportTicketService;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/support")
|
||||
@RequiredArgsConstructor
|
||||
public class SupportController {
|
||||
|
||||
private final SupportTicketService supportTicketService;
|
||||
|
||||
/**
|
||||
* Creates a new support ticket with the first message.
|
||||
*/
|
||||
@PostMapping("/tickets")
|
||||
public ResponseEntity<TicketDto> createTicket(
|
||||
@Valid @RequestBody CreateTicketRequest request) {
|
||||
|
||||
UserA user = UserContext.get();
|
||||
TicketDto ticket = supportTicketService.createTicket(
|
||||
user.getId(),
|
||||
request
|
||||
);
|
||||
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(ticket);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets ticket history for the authenticated user (last 20 tickets).
|
||||
*/
|
||||
@GetMapping("/tickets")
|
||||
public ResponseEntity<List<TicketDto>> getTicketHistory() {
|
||||
|
||||
UserA user = UserContext.get();
|
||||
List<TicketDto> tickets = supportTicketService.getTicketHistory(
|
||||
user.getId()
|
||||
);
|
||||
|
||||
return ResponseEntity.ok(tickets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets ticket details with all messages.
|
||||
*/
|
||||
@GetMapping("/tickets/{ticketId}")
|
||||
public ResponseEntity<TicketDetailDto> getTicketDetail(
|
||||
@PathVariable Long ticketId) {
|
||||
|
||||
UserA user = UserContext.get();
|
||||
TicketDetailDto ticket = supportTicketService.getTicketDetail(
|
||||
user.getId(),
|
||||
ticketId
|
||||
);
|
||||
|
||||
return ResponseEntity.ok(ticket);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a message to an existing ticket.
|
||||
*/
|
||||
@PostMapping("/tickets/{ticketId}/messages")
|
||||
public ResponseEntity<MessageDto> addMessage(
|
||||
@PathVariable Long ticketId,
|
||||
@Valid @RequestBody CreateMessageRequest request) {
|
||||
|
||||
UserA user = UserContext.get();
|
||||
MessageDto message = supportTicketService.addMessage(
|
||||
user.getId(),
|
||||
ticketId,
|
||||
request
|
||||
);
|
||||
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes a ticket.
|
||||
*/
|
||||
@PostMapping("/tickets/{ticketId}/close")
|
||||
public ResponseEntity<Void> closeTicket(
|
||||
@PathVariable Long ticketId) {
|
||||
|
||||
UserA user = UserContext.get();
|
||||
supportTicketService.closeTicket(
|
||||
user.getId(),
|
||||
ticketId
|
||||
);
|
||||
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
}
|
||||
|
||||
100
src/main/java/com/honey/honey/controller/TaskController.java
Normal file
100
src/main/java/com/honey/honey/controller/TaskController.java
Normal file
@@ -0,0 +1,100 @@
|
||||
package com.honey.honey.controller;
|
||||
|
||||
import com.honey.honey.dto.ClaimTaskResponse;
|
||||
import com.honey.honey.dto.DailyBonusStatusDto;
|
||||
import com.honey.honey.dto.RecentBonusClaimDto;
|
||||
import com.honey.honey.dto.TaskDto;
|
||||
import com.honey.honey.model.UserA;
|
||||
import com.honey.honey.security.UserContext;
|
||||
import com.honey.honey.service.LocalizationService;
|
||||
import com.honey.honey.service.TaskService;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/tasks")
|
||||
@RequiredArgsConstructor
|
||||
public class TaskController {
|
||||
|
||||
private final TaskService taskService;
|
||||
private final LocalizationService localizationService;
|
||||
|
||||
/**
|
||||
* Gets all tasks for a specific type (referral, follow, other).
|
||||
* Includes user progress and claim status.
|
||||
*/
|
||||
@GetMapping
|
||||
public List<TaskDto> getTasks(@RequestParam String type) {
|
||||
UserA user = UserContext.get();
|
||||
String languageCode = user.getLanguageCode();
|
||||
if (languageCode == null || languageCode.isEmpty() || "XX".equals(languageCode)) {
|
||||
languageCode = "EN";
|
||||
}
|
||||
return taskService.getTasksByType(user.getId(), type, languageCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets daily bonus status for the current user.
|
||||
* Returns availability status and cooldown time if on cooldown.
|
||||
*/
|
||||
@GetMapping("/daily-bonus")
|
||||
public ResponseEntity<DailyBonusStatusDto> getDailyBonusStatus() {
|
||||
UserA user = UserContext.get();
|
||||
DailyBonusStatusDto status = taskService.getDailyBonusStatus(user.getId());
|
||||
return ResponseEntity.ok(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the 50 most recent daily bonus claims.
|
||||
* Returns claims ordered by claimed_at DESC (most recent first).
|
||||
* Includes user avatar URL, screen name, and formatted claim timestamp with timezone and localized "at" word.
|
||||
*
|
||||
* @param timezone Optional timezone (e.g., "Europe/Kiev"). If not provided, uses UTC.
|
||||
*/
|
||||
@GetMapping("/daily-bonus/recent-claims")
|
||||
public ResponseEntity<List<RecentBonusClaimDto>> getRecentDailyBonusClaims(
|
||||
@RequestParam(required = false) String timezone) {
|
||||
UserA user = UserContext.get();
|
||||
String languageCode = user.getLanguageCode();
|
||||
if (languageCode == null || languageCode.isEmpty() || "XX".equals(languageCode)) {
|
||||
languageCode = "EN";
|
||||
}
|
||||
List<RecentBonusClaimDto> claims = taskService.getRecentDailyBonusClaims(timezone, languageCode);
|
||||
return ResponseEntity.ok(claims);
|
||||
}
|
||||
|
||||
/**
|
||||
* Claims a task for the current user.
|
||||
* Checks if task is completed and gives reward if applicable.
|
||||
* Returns 200 with success status and message.
|
||||
*/
|
||||
@PostMapping("/claim")
|
||||
public ResponseEntity<ClaimTaskResponse> claimTask(@RequestBody ClaimTaskRequest request) {
|
||||
UserA user = UserContext.get();
|
||||
boolean claimed = taskService.claimTask(user.getId(), request.getTaskId());
|
||||
|
||||
if (claimed) {
|
||||
return ResponseEntity.ok(ClaimTaskResponse.builder()
|
||||
.success(true)
|
||||
.message(localizationService.getMessage("task.message.claimed"))
|
||||
.build());
|
||||
} else {
|
||||
return ResponseEntity.ok(ClaimTaskResponse.builder()
|
||||
.success(false)
|
||||
.message(localizationService.getMessage("task.message.notCompleted"))
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ClaimTaskRequest {
|
||||
private Integer taskId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,865 @@
|
||||
package com.honey.honey.controller;
|
||||
|
||||
import com.honey.honey.config.TelegramProperties;
|
||||
import com.honey.honey.dto.TelegramApiResponse;
|
||||
import com.honey.honey.dto.PaymentWebhookRequest;
|
||||
import com.honey.honey.model.UserA;
|
||||
import com.honey.honey.service.FeatureSwitchService;
|
||||
import com.honey.honey.service.PaymentService;
|
||||
import com.honey.honey.service.TelegramBotApiService;
|
||||
import com.honey.honey.service.UserService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
import org.telegram.telegrambots.meta.api.objects.Update;
|
||||
import org.telegram.telegrambots.meta.api.objects.Message;
|
||||
import org.telegram.telegrambots.meta.api.objects.User;
|
||||
import org.telegram.telegrambots.meta.api.objects.CallbackQuery;
|
||||
import org.telegram.telegrambots.meta.api.objects.payments.PreCheckoutQuery;
|
||||
import org.telegram.telegrambots.meta.api.objects.payments.SuccessfulPayment;
|
||||
import org.telegram.telegrambots.meta.api.objects.replykeyboard.InlineKeyboardMarkup;
|
||||
import org.telegram.telegrambots.meta.api.objects.replykeyboard.ReplyKeyboardMarkup;
|
||||
import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.InlineKeyboardButton;
|
||||
import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.KeyboardButton;
|
||||
import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.KeyboardRow;
|
||||
import org.telegram.telegrambots.meta.api.objects.webapp.WebAppInfo;
|
||||
import com.honey.honey.service.LocalizationService;
|
||||
import com.honey.honey.config.LocaleConfig;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Locale;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
|
||||
/**
|
||||
* Webhook controller for receiving Telegram updates directly.
|
||||
* Path: POST /api/telegram/webhook/{token}. Token must match APP_TELEGRAM_WEBHOOK_TOKEN.
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/telegram/webhook")
|
||||
@RequiredArgsConstructor
|
||||
public class TelegramWebhookController {
|
||||
|
||||
private static final String MINI_APP_URL = "https://testforapp.website/test/auth";
|
||||
|
||||
@Value("${app.telegram-webhook.token:}")
|
||||
private String expectedWebhookToken;
|
||||
|
||||
private final UserService userService;
|
||||
private final PaymentService paymentService;
|
||||
private final TelegramProperties telegramProperties;
|
||||
private final LocalizationService localizationService;
|
||||
private final TelegramBotApiService telegramBotApiService;
|
||||
private final FeatureSwitchService featureSwitchService;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* Webhook endpoint for receiving updates from Telegram.
|
||||
* Path token must match app.telegram-webhook.token (APP_TELEGRAM_WEBHOOK_TOKEN).
|
||||
*/
|
||||
@PostMapping("/{token}")
|
||||
public ResponseEntity<?> handleWebhook(@PathVariable String token, @RequestBody Update update, HttpServletRequest httpRequest) {
|
||||
if (expectedWebhookToken.isEmpty() || !expectedWebhookToken.equals(token)) {
|
||||
log.warn("Webhook rejected: invalid or missing token (possible misconfiguration or wrong URL); update dropped");
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||
}
|
||||
try {
|
||||
// Handle callback queries (button clicks)
|
||||
if (update.hasCallbackQuery()) {
|
||||
handleCallbackQuery(update.getCallbackQuery());
|
||||
}
|
||||
|
||||
// Handle message updates (e.g., /start command or Reply Keyboard button clicks)
|
||||
if (update.hasMessage() && update.getMessage().hasText()) {
|
||||
handleMessage(update.getMessage(), httpRequest);
|
||||
}
|
||||
|
||||
// Handle pre-checkout query (before payment confirmation)
|
||||
if (update.hasPreCheckoutQuery()) {
|
||||
handlePreCheckoutQuery(update.getPreCheckoutQuery());
|
||||
}
|
||||
|
||||
// Handle successful payment
|
||||
if (update.hasMessage() && update.getMessage().hasSuccessfulPayment()) {
|
||||
handleSuccessfulPayment(update.getMessage().getSuccessfulPayment(), update.getMessage().getFrom().getId());
|
||||
}
|
||||
|
||||
return ResponseEntity.ok().build();
|
||||
} catch (Exception e) {
|
||||
log.error("Error processing Telegram webhook: {}", e.getMessage(), e);
|
||||
if (update.hasMessage() && update.getMessage().hasText() && update.getMessage().getText().startsWith("/start")) {
|
||||
Long telegramId = update.getMessage().getFrom() != null ? update.getMessage().getFrom().getId() : null;
|
||||
log.warn("Registration attempt failed (webhook error), update dropped: telegramId={}", telegramId);
|
||||
}
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles /start command with optional referral parameter, and Reply Keyboard button clicks.
|
||||
* Format: /start or /start 123 (where 123 is the referral user ID)
|
||||
*/
|
||||
private void handleMessage(Message message, HttpServletRequest httpRequest) {
|
||||
String messageText = message.getText();
|
||||
if (messageText == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
User telegramUser = message.getFrom();
|
||||
Long telegramId = telegramUser.getId();
|
||||
Long chatId = message.getChatId();
|
||||
|
||||
// Handle /start command
|
||||
if (messageText.startsWith("/start")) {
|
||||
handleStartCommand(message, httpRequest, telegramUser, telegramId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle Reply Keyboard button clicks
|
||||
// Priority: 1) saved user language, 2) Telegram user's language_code, 3) default EN
|
||||
String languageCode = "EN"; // Default
|
||||
try {
|
||||
var userOpt = userService.getUserByTelegramId(telegramId);
|
||||
if (userOpt.isPresent() && userOpt.get().getLanguageCode() != null) {
|
||||
languageCode = userOpt.get().getLanguageCode();
|
||||
} else if (telegramUser.getLanguageCode() != null && !telegramUser.getLanguageCode().isEmpty()) {
|
||||
languageCode = telegramUser.getLanguageCode();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Could not get user language, using default: {}", e.getMessage());
|
||||
// Fallback to Telegram user's language_code if available
|
||||
if (telegramUser.getLanguageCode() != null && !telegramUser.getLanguageCode().isEmpty()) {
|
||||
languageCode = telegramUser.getLanguageCode();
|
||||
}
|
||||
}
|
||||
Locale locale = LocaleConfig.languageCodeToLocale(languageCode);
|
||||
|
||||
// Check if message matches Reply Keyboard button text
|
||||
String startSpinningText = "🎰 " + localizationService.getMessage(locale, "bot.button.startSpinning");
|
||||
String usersPayoutsText = "💸 " + localizationService.getMessage(locale, "bot.button.usersPayouts");
|
||||
String infoChannelText = "ℹ️ " + localizationService.getMessage(locale, "bot.button.infoChannel");
|
||||
|
||||
if (messageText.equals(startSpinningText)) {
|
||||
sendStartSpinningMessage(chatId, locale);
|
||||
} else if (messageText.equals(usersPayoutsText)) {
|
||||
sendUsersPayoutsMessage(chatId, locale);
|
||||
} else if (messageText.equals(infoChannelText)) {
|
||||
sendInfoChannelMessage(chatId, locale);
|
||||
} else {
|
||||
// Unknown message (e.g. old "Start Spinning" button or free text): reply and refresh keyboard
|
||||
sendUnrecognizedMessageAndUpdateKeyboard(chatId, locale);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles /start command with optional referral parameter.
|
||||
* Format: /start or /start 123 (where 123 is the referral user ID)
|
||||
*/
|
||||
private void handleStartCommand(Message message, HttpServletRequest httpRequest, User telegramUser, Long telegramId) {
|
||||
String messageText = message.getText();
|
||||
|
||||
log.debug("Received /start command: telegramId={}", telegramId);
|
||||
|
||||
Integer referralUserId = null;
|
||||
|
||||
// Parse referral parameter from /start command
|
||||
// Format: /start or /start 123
|
||||
String[] parts = messageText.split("\\s+", 2);
|
||||
if (parts.length > 1 && !parts[1].trim().isEmpty()) {
|
||||
try {
|
||||
referralUserId = Integer.parseInt(parts[1].trim());
|
||||
log.debug("Parsed referral ID: {}", referralUserId);
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("Invalid referral parameter format: '{}'", parts[1]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user already exists
|
||||
boolean isNewUser = userService.getUserByTelegramId(telegramId).isEmpty();
|
||||
if (isNewUser) {
|
||||
log.info("New user registration via bot - telegramId={}, referralUserId={}",
|
||||
telegramId, referralUserId);
|
||||
}
|
||||
|
||||
// Build tgUserData map similar to what TelegramAuthService.parseInitData returns
|
||||
Map<String, Object> tgUser = new HashMap<>();
|
||||
tgUser.put("id", telegramId);
|
||||
tgUser.put("first_name", telegramUser.getFirstName());
|
||||
tgUser.put("last_name", telegramUser.getLastName());
|
||||
tgUser.put("username", telegramUser.getUserName());
|
||||
tgUser.put("is_premium", telegramUser.getIsPremium() != null && telegramUser.getIsPremium());
|
||||
tgUser.put("language_code", telegramUser.getLanguageCode());
|
||||
// Note: Telegram Bot API User object doesn't have photo_url (only available in WebApp initData)
|
||||
// AvatarService will fetch avatar from Bot API first, photo_url is only used as fallback
|
||||
tgUser.put("photo_url", null);
|
||||
|
||||
Map<String, Object> tgUserData = new HashMap<>();
|
||||
tgUserData.put("user", tgUser);
|
||||
|
||||
// Convert referralUserId to start parameter string (as expected by UserService)
|
||||
String start = referralUserId != null ? String.valueOf(referralUserId) : null;
|
||||
tgUserData.put("start", start);
|
||||
|
||||
try {
|
||||
// Get or create user (handles registration, login update, and referral system)
|
||||
UserA user = userService.getOrCreateUser(tgUserData, httpRequest);
|
||||
log.debug("Bot registration completed: userId={}, telegramId={}, isNewUser={}",
|
||||
user.getId(), user.getTelegramId(), isNewUser);
|
||||
|
||||
// Send welcome message with buttons
|
||||
// Priority: 1) saved user language, 2) Telegram user's language_code, 3) default EN
|
||||
String languageCode = "EN"; // Default
|
||||
if (user.getLanguageCode() != null && !user.getLanguageCode().isEmpty()) {
|
||||
languageCode = user.getLanguageCode();
|
||||
} else if (telegramUser.getLanguageCode() != null && !telegramUser.getLanguageCode().isEmpty()) {
|
||||
languageCode = telegramUser.getLanguageCode();
|
||||
}
|
||||
sendWelcomeMessage(telegramId, languageCode);
|
||||
} catch (Exception e) {
|
||||
log.warn("Registration failed for telegramId={}, user may not receive welcome message: {}", telegramId, e.getMessage());
|
||||
log.error("Error registering user via bot: telegramId={}", telegramId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles callback queries from inline keyboard buttons.
|
||||
*/
|
||||
private void handleCallbackQuery(CallbackQuery callbackQuery) {
|
||||
String data = callbackQuery.getData();
|
||||
Long telegramUserId = callbackQuery.getFrom().getId();
|
||||
Long chatId = callbackQuery.getMessage().getChatId();
|
||||
|
||||
log.debug("Received callback query: data={}, telegramUserId={}", data, telegramUserId);
|
||||
|
||||
// Get user's language for localization
|
||||
// Priority: 1) saved user language, 2) Telegram user's language_code, 3) default EN
|
||||
User telegramUser = callbackQuery.getFrom();
|
||||
String languageCode = "EN"; // Default
|
||||
try {
|
||||
var userOpt = userService.getUserByTelegramId(telegramUserId);
|
||||
if (userOpt.isPresent() && userOpt.get().getLanguageCode() != null) {
|
||||
languageCode = userOpt.get().getLanguageCode();
|
||||
} else if (telegramUser.getLanguageCode() != null && !telegramUser.getLanguageCode().isEmpty()) {
|
||||
languageCode = telegramUser.getLanguageCode();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Could not get user language, using default: {}", e.getMessage());
|
||||
// Fallback to Telegram user's language_code if available
|
||||
if (telegramUser.getLanguageCode() != null && !telegramUser.getLanguageCode().isEmpty()) {
|
||||
languageCode = telegramUser.getLanguageCode();
|
||||
}
|
||||
}
|
||||
|
||||
Locale locale = LocaleConfig.languageCodeToLocale(languageCode);
|
||||
|
||||
try {
|
||||
switch (data) {
|
||||
case "start_spinning":
|
||||
sendStartSpinningMessage(chatId, locale);
|
||||
answerCallbackQuery(callbackQuery.getId(), null);
|
||||
break;
|
||||
case "users_payouts":
|
||||
sendUsersPayoutsMessage(chatId, locale);
|
||||
answerCallbackQuery(callbackQuery.getId(), null);
|
||||
break;
|
||||
case "info_channel":
|
||||
sendInfoChannelMessage(chatId, locale);
|
||||
answerCallbackQuery(callbackQuery.getId(), null);
|
||||
break;
|
||||
default:
|
||||
log.warn("Unknown callback data: {}", data);
|
||||
answerCallbackQuery(callbackQuery.getId(), "Unknown action");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error handling callback query: data={}", data, e);
|
||||
answerCallbackQuery(callbackQuery.getId(), "Error processing request");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the current Reply Keyboard (Start Game, Users payouts, Info channel) for the given locale.
|
||||
*/
|
||||
private ReplyKeyboardMarkup buildReplyKeyboard(Locale locale) {
|
||||
ReplyKeyboardMarkup replyKeyboard = new ReplyKeyboardMarkup();
|
||||
replyKeyboard.setResizeKeyboard(true);
|
||||
replyKeyboard.setOneTimeKeyboard(false);
|
||||
replyKeyboard.setSelective(false);
|
||||
List<KeyboardRow> keyboardRows = new ArrayList<>();
|
||||
KeyboardRow row1 = new KeyboardRow();
|
||||
KeyboardButton startButton = new KeyboardButton();
|
||||
startButton.setText("🎰 " + localizationService.getMessage(locale, "bot.button.startSpinning"));
|
||||
row1.add(startButton);
|
||||
keyboardRows.add(row1);
|
||||
KeyboardRow row2 = new KeyboardRow();
|
||||
KeyboardButton payoutsButton = new KeyboardButton();
|
||||
payoutsButton.setText("💸 " + localizationService.getMessage(locale, "bot.button.usersPayouts"));
|
||||
row2.add(payoutsButton);
|
||||
KeyboardButton infoButton = new KeyboardButton();
|
||||
infoButton.setText("ℹ️ " + localizationService.getMessage(locale, "bot.button.infoChannel"));
|
||||
row2.add(infoButton);
|
||||
keyboardRows.add(row2);
|
||||
replyKeyboard.setKeyboard(keyboardRows);
|
||||
return replyKeyboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends welcome messages: first message with reply keyboard, second message with inline button.
|
||||
*/
|
||||
private void sendWelcomeMessage(Long chatId, String languageCode) {
|
||||
if (telegramProperties.getBotToken() == null || telegramProperties.getBotToken().isEmpty()) {
|
||||
log.warn("Bot token not configured; welcome message not sent for chatId={} (registration flow affected)", chatId);
|
||||
return;
|
||||
}
|
||||
Locale locale = LocaleConfig.languageCodeToLocale(languageCode != null ? languageCode : "EN");
|
||||
ReplyKeyboardMarkup replyKeyboard = buildReplyKeyboard(locale);
|
||||
String firstMessage = localizationService.getMessage(locale, "bot.welcome.firstMessage");
|
||||
sendAnimationWithReplyKeyboard(chatId, firstMessage, replyKeyboard);
|
||||
|
||||
String welcomeText = localizationService.getMessage(locale, "bot.welcome.message");
|
||||
if (featureSwitchService.isStartGameButtonEnabled()) {
|
||||
InlineKeyboardMarkup inlineKeyboard = new InlineKeyboardMarkup();
|
||||
List<List<InlineKeyboardButton>> inlineRows = new ArrayList<>();
|
||||
List<InlineKeyboardButton> inlineRow = new ArrayList<>();
|
||||
InlineKeyboardButton startInlineButton = new InlineKeyboardButton();
|
||||
String startSpinningButtonText = localizationService.getMessage(locale, "bot.button.startSpinningInline");
|
||||
startInlineButton.setText(startSpinningButtonText);
|
||||
WebAppInfo webAppInfo = new WebAppInfo();
|
||||
webAppInfo.setUrl(MINI_APP_URL);
|
||||
startInlineButton.setWebApp(webAppInfo);
|
||||
inlineRow.add(startInlineButton);
|
||||
inlineRows.add(inlineRow);
|
||||
inlineKeyboard.setKeyboard(inlineRows);
|
||||
sendMessage(chatId, welcomeText, inlineKeyboard);
|
||||
} else {
|
||||
sendMessage(chatId, welcomeText, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends message with Start Spinning button.
|
||||
*/
|
||||
private void sendStartSpinningMessage(Long chatId, Locale locale) {
|
||||
String message = localizationService.getMessage(locale, "bot.message.startSpinning");
|
||||
if (!featureSwitchService.isStartGameButtonEnabled()) {
|
||||
sendMessage(chatId, message, null);
|
||||
return;
|
||||
}
|
||||
InlineKeyboardMarkup keyboard = new InlineKeyboardMarkup();
|
||||
List<List<InlineKeyboardButton>> rows = new ArrayList<>();
|
||||
List<InlineKeyboardButton> row = new ArrayList<>();
|
||||
InlineKeyboardButton button = new InlineKeyboardButton();
|
||||
String startSpinningButtonText = localizationService.getMessage(locale, "bot.button.startSpinningInline");
|
||||
button.setText(startSpinningButtonText);
|
||||
WebAppInfo webAppInfo = new WebAppInfo();
|
||||
webAppInfo.setUrl(MINI_APP_URL);
|
||||
button.setWebApp(webAppInfo);
|
||||
row.add(button);
|
||||
rows.add(row);
|
||||
keyboard.setKeyboard(rows);
|
||||
sendMessage(chatId, message, keyboard);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a friendly "unrecognized message" reply and updates the user's reply keyboard to the current one.
|
||||
* Used when the user sends unknown text (e.g. old "Start Spinning" button) so they get the new keyboard.
|
||||
*/
|
||||
private void sendUnrecognizedMessageAndUpdateKeyboard(Long chatId, Locale locale) {
|
||||
String message = localizationService.getMessage(locale, "bot.message.unrecognized");
|
||||
ReplyKeyboardMarkup replyKeyboard = buildReplyKeyboard(locale);
|
||||
sendMessageWithReplyKeyboard(chatId, message, replyKeyboard);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends message with Users payouts button.
|
||||
*/
|
||||
private void sendUsersPayoutsMessage(Long chatId, Locale locale) {
|
||||
String message = localizationService.getMessage(locale, "bot.message.usersPayouts");
|
||||
|
||||
InlineKeyboardMarkup keyboard = new InlineKeyboardMarkup();
|
||||
List<List<InlineKeyboardButton>> rows = new ArrayList<>();
|
||||
List<InlineKeyboardButton> row = new ArrayList<>();
|
||||
|
||||
InlineKeyboardButton button = new InlineKeyboardButton();
|
||||
button.setText(localizationService.getMessage(locale, "bot.button.openChannel"));
|
||||
button.setUrl("https://t.me/win_spin_withdrawals");
|
||||
row.add(button);
|
||||
rows.add(row);
|
||||
|
||||
keyboard.setKeyboard(rows);
|
||||
|
||||
sendMessage(chatId, message, keyboard);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles /paysupport command.
|
||||
*/
|
||||
private void handlePaySupportCommand(Long chatId, User telegramUser, Long telegramId) {
|
||||
// Get user's language for localization
|
||||
// Priority: 1) saved user language, 2) Telegram user's language_code, 3) default EN
|
||||
String languageCode = "EN"; // Default
|
||||
try {
|
||||
var userOpt = userService.getUserByTelegramId(telegramId);
|
||||
if (userOpt.isPresent() && userOpt.get().getLanguageCode() != null) {
|
||||
languageCode = userOpt.get().getLanguageCode();
|
||||
} else if (telegramUser.getLanguageCode() != null && !telegramUser.getLanguageCode().isEmpty()) {
|
||||
languageCode = telegramUser.getLanguageCode();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Could not get user language for /paysupport, using default: {}", e.getMessage());
|
||||
// Fallback to Telegram user's language_code if available
|
||||
if (telegramUser.getLanguageCode() != null && !telegramUser.getLanguageCode().isEmpty()) {
|
||||
languageCode = telegramUser.getLanguageCode();
|
||||
}
|
||||
}
|
||||
|
||||
Locale locale = LocaleConfig.languageCodeToLocale(languageCode);
|
||||
String message = localizationService.getMessage(locale, "bot.message.paySupport");
|
||||
sendMessage(chatId, message, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends message with Info channel button.
|
||||
*/
|
||||
private void sendInfoChannelMessage(Long chatId, Locale locale) {
|
||||
String message = localizationService.getMessage(locale, "bot.message.infoChannel");
|
||||
|
||||
InlineKeyboardMarkup keyboard = new InlineKeyboardMarkup();
|
||||
List<List<InlineKeyboardButton>> rows = new ArrayList<>();
|
||||
List<InlineKeyboardButton> row = new ArrayList<>();
|
||||
|
||||
InlineKeyboardButton button = new InlineKeyboardButton();
|
||||
button.setText(localizationService.getMessage(locale, "bot.button.goToChannel"));
|
||||
button.setUrl("https://t.me/win_spin_news");
|
||||
row.add(button);
|
||||
rows.add(row);
|
||||
|
||||
keyboard.setKeyboard(rows);
|
||||
|
||||
sendMessage(chatId, message, keyboard);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to a chat with inline keyboard.
|
||||
*/
|
||||
private void sendMessage(Long chatId, String text, InlineKeyboardMarkup keyboard) {
|
||||
String botToken = telegramProperties.getBotToken();
|
||||
if (botToken == null || botToken.isEmpty()) {
|
||||
log.error("Bot token is not configured");
|
||||
return;
|
||||
}
|
||||
|
||||
String url = "https://api.telegram.org/bot" + botToken + "/sendMessage";
|
||||
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("chat_id", chatId);
|
||||
requestBody.put("text", text);
|
||||
if (keyboard != null) {
|
||||
// Convert InlineKeyboardMarkup to Map for JSON serialization
|
||||
try {
|
||||
String keyboardJson = objectMapper.writeValueAsString(keyboard);
|
||||
Map<String, Object> keyboardMap = objectMapper.readValue(keyboardJson, Map.class);
|
||||
requestBody.put("reply_markup", keyboardMap);
|
||||
} catch (Exception e) {
|
||||
log.error("Error serializing keyboard: {}", e.getMessage(), e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
|
||||
|
||||
try {
|
||||
ResponseEntity<TelegramApiResponse> response = telegramBotApiService.post(url, entity);
|
||||
if (response != null && response.getBody() != null) {
|
||||
if (!Boolean.TRUE.equals(response.getBody().getOk())) {
|
||||
log.warn("Failed to send message: chatId={}, error={}",
|
||||
chatId, response.getBody().getDescription());
|
||||
}
|
||||
} else if (response != null && !response.getStatusCode().is2xxSuccessful()) {
|
||||
log.error("Failed to send message: chatId={}, status={}", chatId, response.getStatusCode());
|
||||
} else if (response == null) {
|
||||
log.warn("Message not sent (Telegram 429, retry scheduled): chatId={} – may affect registration welcome", chatId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (isTelegramUserUnavailable(e)) {
|
||||
log.warn("Cannot send message: user blocked bot or chat unavailable: chatId={}", chatId);
|
||||
} else {
|
||||
log.error("Error sending message: chatId={}", chatId, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the failure is due to user blocking the bot or chat being unavailable.
|
||||
* These are expected and should be logged at WARN without stack trace.
|
||||
*/
|
||||
private boolean isTelegramUserUnavailable(Throwable t) {
|
||||
if (t instanceof HttpClientErrorException e) {
|
||||
if (e.getStatusCode().value() == 403) {
|
||||
return true;
|
||||
}
|
||||
String body = e.getResponseBodyAsString();
|
||||
return body != null && (
|
||||
body.contains("blocked by the user") ||
|
||||
body.contains("user is deactivated") ||
|
||||
body.contains("chat not found")
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isTelegramUserUnavailableDescription(String description) {
|
||||
return description != null && (
|
||||
description.contains("blocked by the user") ||
|
||||
description.contains("user is deactivated") ||
|
||||
description.contains("chat not found")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message with text and reply keyboard.
|
||||
*/
|
||||
private void sendMessageWithReplyKeyboard(Long chatId, String text, ReplyKeyboardMarkup replyKeyboard) {
|
||||
String botToken = telegramProperties.getBotToken();
|
||||
if (botToken == null || botToken.isEmpty()) {
|
||||
log.error("Bot token is not configured");
|
||||
return;
|
||||
}
|
||||
|
||||
String url = "https://api.telegram.org/bot" + botToken + "/sendMessage";
|
||||
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("chat_id", chatId);
|
||||
requestBody.put("text", text);
|
||||
|
||||
try {
|
||||
String keyboardJson = objectMapper.writeValueAsString(replyKeyboard);
|
||||
Map<String, Object> keyboardMap = objectMapper.readValue(keyboardJson, Map.class);
|
||||
requestBody.put("reply_markup", keyboardMap);
|
||||
} catch (Exception e) {
|
||||
log.error("Error serializing reply keyboard: {}", e.getMessage(), e);
|
||||
return;
|
||||
}
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
|
||||
|
||||
try {
|
||||
ResponseEntity<TelegramApiResponse> response = telegramBotApiService.post(url, entity);
|
||||
if (response != null && response.getBody() != null) {
|
||||
if (!Boolean.TRUE.equals(response.getBody().getOk())) {
|
||||
String desc = response.getBody().getDescription();
|
||||
if (isTelegramUserUnavailableDescription(desc)) {
|
||||
log.warn("Cannot send message: user blocked bot or chat unavailable: chatId={}", chatId);
|
||||
} else {
|
||||
log.error("Failed to send message with reply keyboard: chatId={}, error={}", chatId, desc);
|
||||
}
|
||||
} else {
|
||||
log.info("Message with reply keyboard sent successfully: chatId={}", chatId);
|
||||
}
|
||||
} else if (response != null && !response.getStatusCode().is2xxSuccessful()) {
|
||||
log.error("Failed to send message with reply keyboard: chatId={}, status={}",
|
||||
chatId, response.getStatusCode());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (isTelegramUserUnavailable(e)) {
|
||||
log.warn("Cannot send message: user blocked bot or chat unavailable: chatId={}", chatId);
|
||||
} else {
|
||||
log.error("Error sending message with reply keyboard: chatId={}", chatId, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an animation (MP4 video) with caption text and reply keyboard.
|
||||
* Uses MP4 format as Telegram handles silent MP4s better than GIF files.
|
||||
*/
|
||||
private void sendAnimationWithReplyKeyboard(Long chatId, String caption, ReplyKeyboardMarkup replyKeyboard) {
|
||||
String botToken = telegramProperties.getBotToken();
|
||||
if (botToken == null || botToken.isEmpty()) {
|
||||
log.error("Bot token is not configured");
|
||||
return;
|
||||
}
|
||||
|
||||
String url = "https://api.telegram.org/bot" + botToken + "/sendAnimation";
|
||||
|
||||
try {
|
||||
// Load MP4 from resources (Telegram "GIFs" are actually silent MP4 videos)
|
||||
Resource resource = new ClassPathResource("assets/winspin_5.mp4");
|
||||
if (!resource.exists()) {
|
||||
log.error("MP4 file not found: assets/winspin_5.mp4");
|
||||
// Fallback to text message if MP4 not found
|
||||
sendMessageWithReplyKeyboard(chatId, caption, replyKeyboard);
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] videoBytes = StreamUtils.copyToByteArray(resource.getInputStream());
|
||||
ByteArrayResource videoResource = new ByteArrayResource(videoBytes) {
|
||||
@Override
|
||||
public String getFilename() {
|
||||
return "winspin_5.mp4";
|
||||
}
|
||||
};
|
||||
|
||||
// Create multipart form data
|
||||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||
body.add("chat_id", chatId.toString());
|
||||
body.add("caption", caption);
|
||||
|
||||
// EXPLICITLY SET MIME TYPE FOR THE ANIMATION PART
|
||||
// This is crucial - Telegram needs to know it's a video/mp4
|
||||
HttpHeaders fileHeaders = new HttpHeaders();
|
||||
fileHeaders.setContentType(MediaType.parseMediaType("video/mp4"));
|
||||
HttpEntity<ByteArrayResource> filePart = new HttpEntity<>(videoResource, fileHeaders);
|
||||
body.add("animation", filePart);
|
||||
|
||||
// Add reply keyboard if provided
|
||||
if (replyKeyboard != null) {
|
||||
try {
|
||||
String keyboardJson = objectMapper.writeValueAsString(replyKeyboard);
|
||||
body.add("reply_markup", keyboardJson);
|
||||
} catch (Exception e) {
|
||||
log.error("Error serializing reply keyboard: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||
|
||||
HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers);
|
||||
|
||||
ResponseEntity<TelegramApiResponse> response = telegramBotApiService.post(url, entity);
|
||||
if (response != null && response.getBody() != null) {
|
||||
if (!Boolean.TRUE.equals(response.getBody().getOk())) {
|
||||
String desc = response.getBody().getDescription();
|
||||
if (isTelegramUserUnavailableDescription(desc)) {
|
||||
log.warn("Cannot send animation: user blocked bot or chat unavailable: chatId={}", chatId);
|
||||
} else {
|
||||
log.error("Failed to send animation with reply keyboard: chatId={}, error={}", chatId, desc);
|
||||
sendMessageWithReplyKeyboard(chatId, caption, replyKeyboard);
|
||||
}
|
||||
} else {
|
||||
log.info("Animation with reply keyboard sent successfully: chatId={}", chatId);
|
||||
}
|
||||
} else if (response != null && !response.getStatusCode().is2xxSuccessful()) {
|
||||
log.error("Failed to send animation with reply keyboard: chatId={}, status={}",
|
||||
chatId, response.getStatusCode());
|
||||
sendMessageWithReplyKeyboard(chatId, caption, replyKeyboard);
|
||||
} else if (response == null) {
|
||||
log.warn("Welcome animation delayed (Telegram 429, retry scheduled): chatId={} – registration flow may appear incomplete", chatId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (isTelegramUserUnavailable(e)) {
|
||||
log.warn("Cannot send animation: user blocked bot or chat unavailable: chatId={}", chatId);
|
||||
} else {
|
||||
log.error("Error sending animation with reply keyboard: chatId={}", chatId, e);
|
||||
sendMessageWithReplyKeyboard(chatId, caption, replyKeyboard);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message with only reply keyboard (for setting up persistent keyboard).
|
||||
*/
|
||||
private void sendReplyKeyboardOnly(Long chatId, ReplyKeyboardMarkup replyKeyboard) {
|
||||
String botToken = telegramProperties.getBotToken();
|
||||
if (botToken == null || botToken.isEmpty()) {
|
||||
log.error("Bot token is not configured");
|
||||
return;
|
||||
}
|
||||
|
||||
String url = "https://api.telegram.org/bot" + botToken + "/sendMessage";
|
||||
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("chat_id", chatId);
|
||||
// Telegram requires non-empty text for messages with reply keyboard
|
||||
// Sending with a minimal message - this message won't be visible to users
|
||||
// but is required to set up the persistent keyboard
|
||||
requestBody.put("text", ".");
|
||||
|
||||
try {
|
||||
String keyboardJson = objectMapper.writeValueAsString(replyKeyboard);
|
||||
Map<String, Object> keyboardMap = objectMapper.readValue(keyboardJson, Map.class);
|
||||
requestBody.put("reply_markup", keyboardMap);
|
||||
} catch (Exception e) {
|
||||
log.error("Error serializing reply keyboard: {}", e.getMessage(), e);
|
||||
return;
|
||||
}
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
|
||||
|
||||
try {
|
||||
ResponseEntity<TelegramApiResponse> response = telegramBotApiService.post(url, entity);
|
||||
if (response != null && response.getBody() != null) {
|
||||
if (!Boolean.TRUE.equals(response.getBody().getOk())) {
|
||||
String desc = response.getBody().getDescription();
|
||||
if (isTelegramUserUnavailableDescription(desc)) {
|
||||
log.warn("Cannot send reply keyboard: user blocked bot or chat unavailable: chatId={}", chatId);
|
||||
} else {
|
||||
log.error("Failed to send reply keyboard: chatId={}, error={}", chatId, desc);
|
||||
}
|
||||
} else {
|
||||
log.info("Reply keyboard sent successfully: chatId={}", chatId);
|
||||
}
|
||||
} else if (response != null && !response.getStatusCode().is2xxSuccessful()) {
|
||||
log.error("Failed to send reply keyboard: chatId={}, status={}",
|
||||
chatId, response.getStatusCode());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (isTelegramUserUnavailable(e)) {
|
||||
log.warn("Cannot send reply keyboard: user blocked bot or chat unavailable: chatId={}", chatId);
|
||||
} else {
|
||||
log.error("Error sending reply keyboard: chatId={}", chatId, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Answers a callback query.
|
||||
*/
|
||||
private void answerCallbackQuery(String queryId, String text) {
|
||||
String botToken = telegramProperties.getBotToken();
|
||||
if (botToken == null || botToken.isEmpty()) {
|
||||
log.error("Bot token is not configured");
|
||||
return;
|
||||
}
|
||||
|
||||
String url = "https://api.telegram.org/bot" + botToken + "/answerCallbackQuery";
|
||||
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("callback_query_id", queryId);
|
||||
if (text != null) {
|
||||
requestBody.put("text", text);
|
||||
}
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
|
||||
|
||||
try {
|
||||
ResponseEntity<TelegramApiResponse> response = telegramBotApiService.post(url, entity);
|
||||
if (response != null && response.getBody() != null && !Boolean.TRUE.equals(response.getBody().getOk())) {
|
||||
log.warn("Failed to answer callback query: queryId={}, error={}",
|
||||
queryId, response.getBody().getDescription());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error answering callback query: queryId={}", queryId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles pre-checkout query (before payment confirmation).
|
||||
* Telegram sends this to verify the payment before the user confirms.
|
||||
* We must answer it to approve the payment, otherwise it will expire.
|
||||
*/
|
||||
private void handlePreCheckoutQuery(PreCheckoutQuery preCheckoutQuery) {
|
||||
String queryId = preCheckoutQuery.getId();
|
||||
String invoicePayload = preCheckoutQuery.getInvoicePayload(); // This is our orderId
|
||||
|
||||
log.debug("Pre-checkout query: queryId={}, orderId={}", queryId, invoicePayload);
|
||||
|
||||
// Answer the pre-checkout query to approve the payment
|
||||
// We always approve since we validate on successful payment
|
||||
answerPreCheckoutQuery(queryId, true, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Answers a pre-checkout query to approve or reject the payment.
|
||||
*
|
||||
* @param queryId The pre-checkout query ID
|
||||
* @param ok True to approve, false to reject
|
||||
* @param errorMessage Error message if rejecting (null if approving)
|
||||
*/
|
||||
private void answerPreCheckoutQuery(String queryId, boolean ok, String errorMessage) {
|
||||
String botToken = telegramProperties.getBotToken();
|
||||
if (botToken == null || botToken.isEmpty()) {
|
||||
log.error("Bot token is not configured");
|
||||
return;
|
||||
}
|
||||
|
||||
String url = "https://api.telegram.org/bot" + botToken + "/answerPreCheckoutQuery";
|
||||
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("pre_checkout_query_id", queryId);
|
||||
requestBody.put("ok", ok);
|
||||
if (!ok && errorMessage != null) {
|
||||
requestBody.put("error_message", errorMessage);
|
||||
}
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
|
||||
|
||||
try {
|
||||
log.debug("Answering pre-checkout query: queryId={}, ok={}", queryId, ok);
|
||||
ResponseEntity<TelegramApiResponse> response = telegramBotApiService.post(url, entity);
|
||||
if (response != null && response.getBody() != null && !Boolean.TRUE.equals(response.getBody().getOk())) {
|
||||
log.warn("Failed to answer pre-checkout query: queryId={}, error={}",
|
||||
queryId, response.getBody().getDescription());
|
||||
} else if (response != null && !response.getStatusCode().is2xxSuccessful()) {
|
||||
log.error("Failed to answer pre-checkout query: queryId={}, status={}", queryId, response.getStatusCode());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error answering pre-checkout query: queryId={}", queryId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles successful payment from Telegram.
|
||||
* Processes the payment and credits the user's balance.
|
||||
*/
|
||||
private void handleSuccessfulPayment(SuccessfulPayment successfulPayment, Long telegramUserId) {
|
||||
String invoicePayload = successfulPayment.getInvoicePayload(); // This is our orderId
|
||||
|
||||
// Extract stars amount from total amount
|
||||
// Telegram sends amount in the smallest currency unit (for Stars, it's 1:1)
|
||||
Integer starsAmount = successfulPayment.getTotalAmount().intValue();
|
||||
|
||||
log.info("Payment webhook received: orderId={}, telegramUserId={}, starsAmount={}", invoicePayload, telegramUserId, starsAmount);
|
||||
|
||||
try {
|
||||
// Create webhook request and process payment
|
||||
PaymentWebhookRequest request = new PaymentWebhookRequest();
|
||||
request.setOrderId(invoicePayload);
|
||||
request.setTelegramUserId(telegramUserId);
|
||||
request.setTelegramPaymentChargeId(successfulPayment.getTelegramPaymentChargeId());
|
||||
request.setTelegramProviderPaymentChargeId(successfulPayment.getProviderPaymentChargeId());
|
||||
request.setStarsAmount(starsAmount);
|
||||
|
||||
boolean processed = paymentService.processPaymentWebhook(request);
|
||||
if (!processed) {
|
||||
log.warn("Payment already processed: orderId={}", invoicePayload);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error processing payment webhook: orderId={}", invoicePayload, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.honey.honey.controller;
|
||||
|
||||
import com.honey.honey.dto.TransactionDto;
|
||||
import com.honey.honey.model.UserA;
|
||||
import com.honey.honey.security.UserContext;
|
||||
import com.honey.honey.service.TransactionService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* Controller for transaction history operations.
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/transactions")
|
||||
@RequiredArgsConstructor
|
||||
public class TransactionController {
|
||||
|
||||
private final TransactionService transactionService;
|
||||
|
||||
/**
|
||||
* Gets transaction history for the current user.
|
||||
* Returns 50 transactions per page, ordered by creation time descending (newest first).
|
||||
*
|
||||
* @param page Page number (0-indexed, defaults to 0)
|
||||
* @param timezone Optional timezone (e.g., "Europe/London"). If not provided, uses UTC.
|
||||
* @return Page of transactions
|
||||
*/
|
||||
@GetMapping
|
||||
public ResponseEntity<Page<TransactionDto>> getTransactions(
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(required = false) String timezone) {
|
||||
UserA user = UserContext.get();
|
||||
Integer userId = user.getId();
|
||||
String languageCode = user.getLanguageCode();
|
||||
if (languageCode == null || languageCode.isEmpty() || "XX".equals(languageCode)) {
|
||||
languageCode = "EN";
|
||||
}
|
||||
// Note: languageCode is still used for date formatting (localized "at" word)
|
||||
// Transaction type localization is now handled in the frontend
|
||||
Page<TransactionDto> transactions = transactionService.getUserTransactions(userId, page, timezone, languageCode);
|
||||
return ResponseEntity.ok(transactions);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.honey.honey.controller;
|
||||
|
||||
import com.honey.honey.dto.UserCheckDto;
|
||||
import com.honey.honey.model.UserA;
|
||||
import com.honey.honey.model.UserB;
|
||||
import com.honey.honey.model.UserD;
|
||||
import com.honey.honey.repository.PaymentRepository;
|
||||
import com.honey.honey.repository.UserARepository;
|
||||
import com.honey.honey.repository.UserBRepository;
|
||||
import com.honey.honey.repository.UserDRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Controller for user check endpoint (open endpoint for external applications).
|
||||
* Path token is validated against app.check-user.token (APP_CHECK_USER_TOKEN). No user auth.
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/check_user")
|
||||
@RequiredArgsConstructor
|
||||
public class UserCheckController {
|
||||
|
||||
@Value("${app.check-user.token:}")
|
||||
private String expectedToken;
|
||||
|
||||
private final UserARepository userARepository;
|
||||
private final UserBRepository userBRepository;
|
||||
private final UserDRepository userDRepository;
|
||||
private final PaymentRepository paymentRepository;
|
||||
|
||||
/**
|
||||
* Gets user information by Telegram ID.
|
||||
* Path: /api/check_user/{token}/{telegramId}. Token must match APP_CHECK_USER_TOKEN.
|
||||
*
|
||||
* @param token Secret token from path (must match config)
|
||||
* @param telegramId The Telegram ID of the user
|
||||
* @return 200 with user info (found=true) or 200 with found=false when user not found; 403 if token invalid
|
||||
*/
|
||||
@GetMapping("/{token}/{telegramId}")
|
||||
public ResponseEntity<UserCheckDto> checkUser(@PathVariable String token, @PathVariable Long telegramId) {
|
||||
if (expectedToken.isEmpty() || !expectedToken.equals(token)) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||
}
|
||||
try {
|
||||
// Find user by telegram_id
|
||||
Optional<UserA> userAOpt = userARepository.findByTelegramId(telegramId);
|
||||
|
||||
if (userAOpt.isEmpty()) {
|
||||
log.debug("User not found for telegramId={}", telegramId);
|
||||
UserCheckDto notFoundResponse = UserCheckDto.builder().found(false).build();
|
||||
return ResponseEntity.ok(notFoundResponse);
|
||||
}
|
||||
|
||||
UserA userA = userAOpt.get();
|
||||
Integer userId = userA.getId();
|
||||
|
||||
// Get balance_a from db_users_b
|
||||
Optional<UserB> userBOpt = userBRepository.findById(userId);
|
||||
Long balanceA = userBOpt.map(UserB::getBalanceA).orElse(0L);
|
||||
// Convert to tickets (balance_a / 1,000,000)
|
||||
Double tickets = balanceA / 1_000_000.0;
|
||||
|
||||
// Get referer_id_1 from db_users_d
|
||||
Optional<UserD> userDOpt = userDRepository.findById(userId);
|
||||
Integer refererId = userDOpt.map(UserD::getRefererId1).orElse(0);
|
||||
// Return 0 if refererId is 0 or negative (not set)
|
||||
if (refererId <= 0) {
|
||||
refererId = 0;
|
||||
}
|
||||
|
||||
// Sum completed payments stars_amount
|
||||
Integer depositTotal = paymentRepository.sumCompletedStarsAmountByUserId(userId);
|
||||
|
||||
// Build response
|
||||
UserCheckDto response = UserCheckDto.builder()
|
||||
.found(true)
|
||||
.dateReg(userA.getDateReg())
|
||||
.tickets(tickets)
|
||||
.depositTotal(depositTotal)
|
||||
.refererId(refererId)
|
||||
.build();
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error checking user for telegramId={}", telegramId, e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
146
src/main/java/com/honey/honey/controller/UserController.java
Normal file
146
src/main/java/com/honey/honey/controller/UserController.java
Normal file
@@ -0,0 +1,146 @@
|
||||
package com.honey.honey.controller;
|
||||
|
||||
import com.honey.honey.dto.ReferralDto;
|
||||
import com.honey.honey.dto.UserDto;
|
||||
import com.honey.honey.model.UserA;
|
||||
import com.honey.honey.model.UserB;
|
||||
import com.honey.honey.repository.UserBRepository;
|
||||
import com.honey.honey.security.UserContext;
|
||||
import com.honey.honey.service.AvatarService;
|
||||
import com.honey.honey.service.FeatureSwitchService;
|
||||
import com.honey.honey.service.LocalizationService;
|
||||
import com.honey.honey.service.UserService;
|
||||
import com.honey.honey.util.IpUtils;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
@RequiredArgsConstructor
|
||||
public class UserController {
|
||||
|
||||
private final UserService userService;
|
||||
private final UserBRepository userBRepository;
|
||||
private final AvatarService avatarService;
|
||||
private final LocalizationService localizationService;
|
||||
private final FeatureSwitchService featureSwitchService;
|
||||
|
||||
@GetMapping("/current")
|
||||
public UserDto getCurrentUser() {
|
||||
UserA user = UserContext.get();
|
||||
|
||||
// Convert IP from byte[] to string for display
|
||||
String ipAddress = IpUtils.bytesToIp(user.getIp());
|
||||
|
||||
// Get balance
|
||||
Long balanceA = userBRepository.findById(user.getId())
|
||||
.map(UserB::getBalanceA)
|
||||
.orElse(0L);
|
||||
|
||||
// Generate avatar URL on-the-fly (deterministic from userId)
|
||||
String avatarUrl = avatarService.getAvatarUrl(user.getId());
|
||||
|
||||
return UserDto.builder()
|
||||
.id(user.getId())
|
||||
.telegram_id(user.getTelegramId())
|
||||
.username(user.getTelegramName())
|
||||
.screenName(user.getScreenName())
|
||||
.dateReg(user.getDateReg())
|
||||
.ip(ipAddress)
|
||||
.balanceA(balanceA)
|
||||
.avatarUrl(avatarUrl)
|
||||
.languageCode(user.getLanguageCode())
|
||||
.paymentEnabled(featureSwitchService.isPaymentEnabled())
|
||||
.payoutEnabled(featureSwitchService.isPayoutEnabled())
|
||||
.promotionsEnabled(featureSwitchService.isPromotionsEnabled())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates user's language code.
|
||||
* Called when user changes language in app header.
|
||||
*/
|
||||
@PutMapping("/language")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
public void updateLanguage(@RequestBody UpdateLanguageRequest request) {
|
||||
UserA user = UserContext.get();
|
||||
userService.updateLanguageCode(user.getId(), request.getLanguageCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds deposit amount to user's balance_a.
|
||||
* For now, this is a mock implementation that directly adds to balance.
|
||||
* Will be replaced with payment integration later.
|
||||
*/
|
||||
@PostMapping("/deposit")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
public void deposit(@RequestBody DepositRequest request) {
|
||||
UserA user = UserContext.get();
|
||||
|
||||
// Frontend sends amount already in bigint format (no conversion needed)
|
||||
Long depositAmount = request.getAmount();
|
||||
if (depositAmount == null || depositAmount <= 0) {
|
||||
throw new IllegalArgumentException(localizationService.getMessage("user.error.depositAmountInvalid"));
|
||||
}
|
||||
|
||||
UserB userB = userBRepository.findById(user.getId())
|
||||
.orElseThrow(() -> new IllegalStateException(localizationService.getMessage("user.error.balanceNotFound")));
|
||||
|
||||
// Add to balance
|
||||
userB.setBalanceA(userB.getBalanceA() + depositAmount);
|
||||
|
||||
// Update deposit statistics
|
||||
userB.setDepositTotal(userB.getDepositTotal() + depositAmount);
|
||||
userB.setDepositCount(userB.getDepositCount() + 1);
|
||||
|
||||
userBRepository.save(userB);
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class UpdateLanguageRequest {
|
||||
private String languageCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets referrals for a specific level with pagination.
|
||||
* Always returns 50 results per page.
|
||||
*
|
||||
* @param level The referral level (1, 2, or 3)
|
||||
* @param page Page number (0-indexed, defaults to 0)
|
||||
* @return Page of referrals with name and commission
|
||||
*/
|
||||
@GetMapping("/referrals")
|
||||
public ReferralsResponse getReferrals(
|
||||
@RequestParam Integer level,
|
||||
@RequestParam(defaultValue = "0") Integer page) {
|
||||
UserA user = UserContext.get();
|
||||
|
||||
Page<ReferralDto> referralsPage = userService.getReferrals(user.getId(), level, page);
|
||||
|
||||
return new ReferralsResponse(
|
||||
referralsPage.getContent(),
|
||||
referralsPage.getNumber(),
|
||||
referralsPage.getTotalPages(),
|
||||
referralsPage.getTotalElements()
|
||||
);
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class DepositRequest {
|
||||
private Long amount; // Amount in bigint format (frontend converts before sending)
|
||||
}
|
||||
|
||||
@Data
|
||||
@RequiredArgsConstructor
|
||||
public static class ReferralsResponse {
|
||||
private final java.util.List<ReferralDto> referrals;
|
||||
private final Integer currentPage;
|
||||
private final Integer totalPages;
|
||||
private final Long totalElements;
|
||||
}
|
||||
}
|
||||
10
src/main/java/com/honey/honey/dto/AdminLoginRequest.java
Normal file
10
src/main/java/com/honey/honey/dto/AdminLoginRequest.java
Normal file
@@ -0,0 +1,10 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class AdminLoginRequest {
|
||||
private String username;
|
||||
private String password;
|
||||
}
|
||||
|
||||
13
src/main/java/com/honey/honey/dto/AdminLoginResponse.java
Normal file
13
src/main/java/com/honey/honey/dto/AdminLoginResponse.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public class AdminLoginResponse {
|
||||
private String token;
|
||||
private String username;
|
||||
private String role;
|
||||
}
|
||||
|
||||
31
src/main/java/com/honey/honey/dto/AdminMasterDto.java
Normal file
31
src/main/java/com/honey/honey/dto/AdminMasterDto.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AdminMasterDto {
|
||||
private Integer id;
|
||||
private String screenName;
|
||||
/** Level 1 referrals count (from master's UserD row). */
|
||||
private Integer referals1;
|
||||
/** Level 2 referrals count. */
|
||||
private Integer referals2;
|
||||
/** Level 3 referrals count. */
|
||||
private Integer referals3;
|
||||
/** Total users with master_id = this master's id. */
|
||||
private Long totalReferrals;
|
||||
/** Sum of deposit_total of all referrals, in USD (divided by 1e9). */
|
||||
private BigDecimal depositTotalUsd;
|
||||
/** Sum of withdraw_total of all referrals, in USD (divided by 1e9). */
|
||||
private BigDecimal withdrawTotalUsd;
|
||||
/** depositTotalUsd - withdrawTotalUsd. */
|
||||
private BigDecimal profitUsd;
|
||||
}
|
||||
27
src/main/java/com/honey/honey/dto/AdminPaymentDto.java
Normal file
27
src/main/java/com/honey/honey/dto/AdminPaymentDto.java
Normal file
@@ -0,0 +1,27 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AdminPaymentDto {
|
||||
private Long id;
|
||||
private Integer userId;
|
||||
private String userName;
|
||||
private String orderId;
|
||||
private Integer starsAmount;
|
||||
private Long ticketsAmount;
|
||||
private String status;
|
||||
private String telegramPaymentChargeId;
|
||||
private String telegramProviderPaymentChargeId;
|
||||
private Instant createdAt;
|
||||
private Instant completedAt;
|
||||
}
|
||||
|
||||
28
src/main/java/com/honey/honey/dto/AdminPayoutDto.java
Normal file
28
src/main/java/com/honey/honey/dto/AdminPayoutDto.java
Normal file
@@ -0,0 +1,28 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AdminPayoutDto {
|
||||
private Long id;
|
||||
private Integer userId;
|
||||
private String userName;
|
||||
private String username;
|
||||
private String type;
|
||||
private String giftName;
|
||||
private Long total;
|
||||
private Integer starsAmount;
|
||||
private Integer quantity;
|
||||
private String status;
|
||||
private Instant createdAt;
|
||||
private Instant resolvedAt;
|
||||
}
|
||||
|
||||
23
src/main/java/com/honey/honey/dto/AdminPromotionDto.java
Normal file
23
src/main/java/com/honey/honey/dto/AdminPromotionDto.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AdminPromotionDto {
|
||||
private Integer id;
|
||||
private String type;
|
||||
private Instant startTime;
|
||||
private Instant endTime;
|
||||
private String status;
|
||||
private Long totalReward;
|
||||
private Instant createdAt;
|
||||
private Instant updatedAt;
|
||||
}
|
||||
25
src/main/java/com/honey/honey/dto/AdminPromotionRequest.java
Normal file
25
src/main/java/com/honey/honey/dto/AdminPromotionRequest.java
Normal file
@@ -0,0 +1,25 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AdminPromotionRequest {
|
||||
@NotNull
|
||||
private String type;
|
||||
@NotNull
|
||||
private Instant startTime;
|
||||
@NotNull
|
||||
private Instant endTime;
|
||||
@NotNull
|
||||
private String status;
|
||||
private Long totalReward;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AdminPromotionRewardDto {
|
||||
private Integer id;
|
||||
private Integer promoId;
|
||||
private Integer place;
|
||||
/** Reward in bigint (1 ticket = 1_000_000). */
|
||||
private Long reward;
|
||||
private Instant createdAt;
|
||||
private Instant updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AdminPromotionRewardRequest {
|
||||
@NotNull
|
||||
private Integer place;
|
||||
@NotNull
|
||||
private Long reward; // bigint, 1 ticket = 1_000_000
|
||||
}
|
||||
21
src/main/java/com/honey/honey/dto/AdminPromotionUserDto.java
Normal file
21
src/main/java/com/honey/honey/dto/AdminPromotionUserDto.java
Normal file
@@ -0,0 +1,21 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AdminPromotionUserDto {
|
||||
private Integer promoId;
|
||||
private Integer userId;
|
||||
/** Points as ticket count, 2 decimal places (e.g. 100.25). */
|
||||
private BigDecimal points;
|
||||
private Instant updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AdminPromotionUserPointsRequest {
|
||||
@NotNull
|
||||
private BigDecimal points; // ticket count, 2 decimal places
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AdminSupportMessageDto {
|
||||
private Long id;
|
||||
private Integer userId;
|
||||
private String userName;
|
||||
private String message;
|
||||
private Instant createdAt;
|
||||
private Boolean isAdmin; // true if sent by admin
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AdminSupportTicketDetailDto {
|
||||
private Long id;
|
||||
private Integer userId;
|
||||
private String userName;
|
||||
private String subject;
|
||||
private String status;
|
||||
private Instant createdAt;
|
||||
private Instant updatedAt;
|
||||
private List<AdminSupportMessageDto> messages;
|
||||
}
|
||||
|
||||
26
src/main/java/com/honey/honey/dto/AdminSupportTicketDto.java
Normal file
26
src/main/java/com/honey/honey/dto/AdminSupportTicketDto.java
Normal file
@@ -0,0 +1,26 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AdminSupportTicketDto {
|
||||
private Long id;
|
||||
private Integer userId;
|
||||
private String userName;
|
||||
private String subject;
|
||||
private String status;
|
||||
private Instant createdAt;
|
||||
private Instant updatedAt;
|
||||
private Long messageCount;
|
||||
private String lastMessagePreview;
|
||||
private Instant lastMessageAt;
|
||||
}
|
||||
|
||||
21
src/main/java/com/honey/honey/dto/AdminTaskClaimDto.java
Normal file
21
src/main/java/com/honey/honey/dto/AdminTaskClaimDto.java
Normal file
@@ -0,0 +1,21 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AdminTaskClaimDto {
|
||||
private Long id;
|
||||
private Integer taskId;
|
||||
private String taskTitle;
|
||||
private String taskType;
|
||||
private Instant claimedAt;
|
||||
}
|
||||
|
||||
21
src/main/java/com/honey/honey/dto/AdminTransactionDto.java
Normal file
21
src/main/java/com/honey/honey/dto/AdminTransactionDto.java
Normal file
@@ -0,0 +1,21 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AdminTransactionDto {
|
||||
private Long id;
|
||||
private Long amount; // In bigint format
|
||||
private String type;
|
||||
private Integer taskId;
|
||||
private Instant createdAt;
|
||||
}
|
||||
|
||||
52
src/main/java/com/honey/honey/dto/AdminUserDetailDto.java
Normal file
52
src/main/java/com/honey/honey/dto/AdminUserDetailDto.java
Normal file
@@ -0,0 +1,52 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AdminUserDetailDto {
|
||||
// Basic Info
|
||||
private Integer id;
|
||||
private String screenName;
|
||||
private Long telegramId;
|
||||
private String telegramName;
|
||||
private Integer isPremium;
|
||||
private String languageCode;
|
||||
private String countryCode;
|
||||
private String deviceCode;
|
||||
private String avatarUrl;
|
||||
private Integer dateReg;
|
||||
private Integer dateLogin;
|
||||
private Integer banned;
|
||||
/** IP address as string (e.g. xxx.xxx.xxx.xxx), converted from varbinary in DB. */
|
||||
private String ipAddress;
|
||||
|
||||
// Balance Info
|
||||
private Long balanceA;
|
||||
private Long depositTotal;
|
||||
private Integer depositCount;
|
||||
private Long withdrawTotal;
|
||||
private Integer withdrawCount;
|
||||
/** Total deposits in USD (CRYPTO only). */
|
||||
private java.math.BigDecimal depositTotalUsd;
|
||||
/** Total withdrawals in USD (CRYPTO only). */
|
||||
private java.math.BigDecimal withdrawTotalUsd;
|
||||
/** When true, the user cannot create any payout request. */
|
||||
private Boolean withdrawalsDisabled;
|
||||
|
||||
// Referral Info
|
||||
private Integer referralCount;
|
||||
private Long totalCommissionsEarned;
|
||||
/** Total commissions earned in USD (converted from tickets). */
|
||||
private java.math.BigDecimal totalCommissionsEarnedUsd;
|
||||
private Integer masterId;
|
||||
private List<ReferralLevelDto> referralLevels;
|
||||
}
|
||||
|
||||
41
src/main/java/com/honey/honey/dto/AdminUserDto.java
Normal file
41
src/main/java/com/honey/honey/dto/AdminUserDto.java
Normal file
@@ -0,0 +1,41 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AdminUserDto {
|
||||
private Integer id;
|
||||
private String screenName;
|
||||
private Long telegramId;
|
||||
private String telegramName;
|
||||
private Long balanceA;
|
||||
private Long balanceB;
|
||||
private Long depositTotal;
|
||||
private Integer depositCount;
|
||||
private Long withdrawTotal;
|
||||
private Integer withdrawCount;
|
||||
private Integer dateReg;
|
||||
private Integer dateLogin;
|
||||
private Integer banned;
|
||||
private String countryCode;
|
||||
private String languageCode;
|
||||
private Integer referralCount; // Total referrals across all levels
|
||||
private Long totalCommissionsEarned; // Total commissions earned from referrals
|
||||
/** Profit in tickets (bigint): depositTotal - withdrawTotal */
|
||||
private Long profit;
|
||||
/** USD from db_users_b: depositTotal (tickets/1000) */
|
||||
private BigDecimal depositTotalUsd;
|
||||
/** USD from db_users_b: withdrawTotal (tickets/1000) */
|
||||
private BigDecimal withdrawTotalUsd;
|
||||
/** USD from db_users_b: profit (tickets/1000) */
|
||||
private BigDecimal profitUsd;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class BalanceAdjustmentRequest {
|
||||
@NotNull(message = "Balance type is required")
|
||||
private BalanceType balanceType; // A or B
|
||||
|
||||
@NotNull(message = "Amount is required")
|
||||
private Long amount; // In bigint format (tickets * 1,000,000)
|
||||
|
||||
@NotNull(message = "Operation is required")
|
||||
private OperationType operation; // ADD or SUBTRACT
|
||||
|
||||
@NotBlank(message = "Reason is required")
|
||||
private String reason; // Reason for adjustment (for audit log)
|
||||
|
||||
public enum BalanceType {
|
||||
A, B
|
||||
}
|
||||
|
||||
public enum OperationType {
|
||||
ADD, SUBTRACT
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class BalanceAdjustmentResponse {
|
||||
private Long newBalanceA;
|
||||
private Long newBalanceB;
|
||||
private Long previousBalanceA;
|
||||
private Long previousBalanceB;
|
||||
private Long adjustmentAmount;
|
||||
private String message;
|
||||
}
|
||||
|
||||
19
src/main/java/com/honey/honey/dto/BalanceUpdateDto.java
Normal file
19
src/main/java/com/honey/honey/dto/BalanceUpdateDto.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class BalanceUpdateDto {
|
||||
private Long balanceA; // Balance in bigint format (database format)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
37
src/main/java/com/honey/honey/dto/BotRegisterRequest.java
Normal file
37
src/main/java/com/honey/honey/dto/BotRegisterRequest.java
Normal file
@@ -0,0 +1,37 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class BotRegisterRequest {
|
||||
@JsonProperty("telegram_id")
|
||||
private Long telegramId;
|
||||
|
||||
@JsonProperty("first_name")
|
||||
private String firstName;
|
||||
|
||||
@JsonProperty("last_name")
|
||||
private String lastName;
|
||||
|
||||
private String username;
|
||||
|
||||
@JsonProperty("is_premium")
|
||||
private Boolean isPremium;
|
||||
|
||||
@JsonProperty("language_code")
|
||||
private String languageCode;
|
||||
|
||||
@JsonProperty("photo_url")
|
||||
private String photoUrl;
|
||||
|
||||
@JsonProperty("referral_user_id")
|
||||
private Integer referralUserId;
|
||||
}
|
||||
|
||||
25
src/main/java/com/honey/honey/dto/BotRegisterResponse.java
Normal file
25
src/main/java/com/honey/honey/dto/BotRegisterResponse.java
Normal file
@@ -0,0 +1,25 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class BotRegisterResponse {
|
||||
@JsonProperty("user_id")
|
||||
private Integer userId;
|
||||
|
||||
@JsonProperty("is_new_user")
|
||||
private Boolean isNewUser;
|
||||
|
||||
private String message;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
19
src/main/java/com/honey/honey/dto/ClaimTaskResponse.java
Normal file
19
src/main/java/com/honey/honey/dto/ClaimTaskResponse.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ClaimTaskResponse {
|
||||
private boolean success;
|
||||
private String message;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* Request body for POST /api/payments/crypto-withdrawal.
|
||||
*/
|
||||
@Data
|
||||
public class CreateCryptoWithdrawalRequest {
|
||||
|
||||
@NotNull(message = "pid is required")
|
||||
private Integer pid;
|
||||
|
||||
@NotBlank(message = "wallet is required")
|
||||
private String wallet;
|
||||
|
||||
/** Tickets amount in bigint format (tickets * 1_000_000). */
|
||||
@NotNull(message = "total is required")
|
||||
private Long total;
|
||||
}
|
||||
22
src/main/java/com/honey/honey/dto/CreateMessageRequest.java
Normal file
22
src/main/java/com/honey/honey/dto/CreateMessageRequest.java
Normal file
@@ -0,0 +1,22 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CreateMessageRequest {
|
||||
|
||||
@NotBlank(message = "Message is required")
|
||||
@Size(min = 3, max = 2000, message = "Message must be between 3 and 2000 characters")
|
||||
private String message;
|
||||
}
|
||||
|
||||
|
||||
|
||||
13
src/main/java/com/honey/honey/dto/CreatePaymentRequest.java
Normal file
13
src/main/java/com/honey/honey/dto/CreatePaymentRequest.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class CreatePaymentRequest {
|
||||
private Integer starsAmount; // Amount in Stars (legacy)
|
||||
private Double usdAmount; // USD as decimal, e.g. 3.25 (crypto)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
14
src/main/java/com/honey/honey/dto/CreatePayoutRequest.java
Normal file
14
src/main/java/com/honey/honey/dto/CreatePayoutRequest.java
Normal file
@@ -0,0 +1,14 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class CreatePayoutRequest {
|
||||
private String username;
|
||||
private Long total; // Tickets amount in bigint format
|
||||
private Integer starsAmount; // Stars amount (for STARS type)
|
||||
private String type; // "STARS" or "GIFT"
|
||||
private String giftName; // Gift name (for GIFT type): "HEART", "BEAR", etc.
|
||||
private Integer quantity; // Quantity of gifts/stars (1-100, default 1)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class CreateSessionRequest {
|
||||
private String initData;
|
||||
}
|
||||
|
||||
16
src/main/java/com/honey/honey/dto/CreateSessionResponse.java
Normal file
16
src/main/java/com/honey/honey/dto/CreateSessionResponse.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CreateSessionResponse {
|
||||
private String access_token;
|
||||
private Integer expires_in;
|
||||
}
|
||||
|
||||
26
src/main/java/com/honey/honey/dto/CreateTicketRequest.java
Normal file
26
src/main/java/com/honey/honey/dto/CreateTicketRequest.java
Normal file
@@ -0,0 +1,26 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CreateTicketRequest {
|
||||
|
||||
@NotBlank(message = "Subject is required")
|
||||
@Size(min = 5, max = 100, message = "Subject must be between 5 and 100 characters")
|
||||
private String subject;
|
||||
|
||||
@NotBlank(message = "Message is required")
|
||||
@Size(min = 3, max = 2000, message = "Message must be between 3 and 2000 characters")
|
||||
private String message;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** Response from external GET /api/v1/deposit-methods */
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class CryptoDepositMethodsResponse {
|
||||
|
||||
@JsonProperty("request_info")
|
||||
private RequestInfo requestInfo;
|
||||
|
||||
@JsonProperty("result")
|
||||
private Result result;
|
||||
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class RequestInfo {
|
||||
@JsonProperty("error_code")
|
||||
private Integer errorCode;
|
||||
@JsonProperty("error_message")
|
||||
private String errorMessage;
|
||||
}
|
||||
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class Result {
|
||||
@JsonProperty("active_methods")
|
||||
private List<ActiveMethod> activeMethods;
|
||||
@JsonProperty("hash")
|
||||
private String hash;
|
||||
}
|
||||
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class ActiveMethod {
|
||||
@JsonProperty("pid")
|
||||
private Integer pid;
|
||||
@JsonProperty("name")
|
||||
private String name;
|
||||
@JsonProperty("network")
|
||||
private String network;
|
||||
@JsonProperty("example")
|
||||
private String example;
|
||||
@JsonProperty("min_deposit_sum")
|
||||
private Double minDepositSum;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Minimal response for POST /api/payments/crypto-withdrawal.
|
||||
* Exposes only id and status; no internal or PII fields.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CryptoWithdrawalResponse {
|
||||
private Long id;
|
||||
private String status;
|
||||
}
|
||||
19
src/main/java/com/honey/honey/dto/DailyBonusStatusDto.java
Normal file
19
src/main/java/com/honey/honey/dto/DailyBonusStatusDto.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class DailyBonusStatusDto {
|
||||
private Integer taskId;
|
||||
private Boolean available; // true if bonus can be claimed, false if on cooldown
|
||||
private Long cooldownSeconds; // Remaining cooldown time in seconds (null if available)
|
||||
private Long rewardAmount; // Reward amount in bigint format (1 ticket = 1000000)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Request body for external POST api/v1/deposit-address.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
public class DepositAddressApiRequest {
|
||||
|
||||
@JsonProperty("pid")
|
||||
private Integer pid;
|
||||
|
||||
@JsonProperty("amount_usd")
|
||||
private Double amountUsd;
|
||||
|
||||
@JsonProperty("user_data")
|
||||
private UserData userData;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public static class UserData {
|
||||
@JsonProperty("internal_id")
|
||||
private Integer internalId;
|
||||
@JsonProperty("screen_name")
|
||||
private String screenName;
|
||||
@JsonProperty("tg_username")
|
||||
private String tgUsername;
|
||||
@JsonProperty("tg_id")
|
||||
private String tgId;
|
||||
@JsonProperty("country_code")
|
||||
private String countryCode;
|
||||
@JsonProperty("device_code")
|
||||
private String deviceCode;
|
||||
@JsonProperty("language_code")
|
||||
private String languageCode;
|
||||
@JsonProperty("user_ip")
|
||||
private String userIp;
|
||||
}
|
||||
}
|
||||
13
src/main/java/com/honey/honey/dto/DepositAddressRequest.java
Normal file
13
src/main/java/com/honey/honey/dto/DepositAddressRequest.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Request from frontend to get a crypto deposit address.
|
||||
* usdAmount: decimal, e.g. 3.25 USD.
|
||||
*/
|
||||
@Data
|
||||
public class DepositAddressRequest {
|
||||
private Integer pid; // PID from deposit-methods
|
||||
private Double usdAmount; // USD as decimal, e.g. 3.25
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Response from external POST api/v1/deposit-address (and returned to frontend).
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class DepositAddressResponse {
|
||||
|
||||
@JsonProperty("request_info")
|
||||
private RequestInfo requestInfo;
|
||||
|
||||
@JsonProperty("result")
|
||||
private Result result;
|
||||
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class RequestInfo {
|
||||
@JsonProperty("error_code")
|
||||
private Integer errorCode;
|
||||
@JsonProperty("error_message")
|
||||
private String errorMessage;
|
||||
}
|
||||
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class Result {
|
||||
@JsonProperty("ps_id")
|
||||
private Integer psId;
|
||||
@JsonProperty("name")
|
||||
private String name;
|
||||
@JsonProperty("network")
|
||||
private String network;
|
||||
@JsonProperty("address")
|
||||
private String address;
|
||||
@JsonProperty("amount_coins")
|
||||
private String amountCoins;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Result returned to frontend after getting deposit address from crypto API.
|
||||
* No payment record is created at this step.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
public class DepositAddressResultDto {
|
||||
private String address;
|
||||
private String amountCoins;
|
||||
private String name;
|
||||
private String network;
|
||||
private Integer psId;
|
||||
/** Minimum deposit for this method (from crypto_deposit_methods.min_deposit_sum), for display on Payment Confirmation. Value as in DB, e.g. 2.50, 40.00. */
|
||||
private Double minAmount;
|
||||
}
|
||||
32
src/main/java/com/honey/honey/dto/DepositMethodsDto.java
Normal file
32
src/main/java/com/honey/honey/dto/DepositMethodsDto.java
Normal file
@@ -0,0 +1,32 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/** Response for GET /api/payments/deposit-methods */
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class DepositMethodsDto {
|
||||
|
||||
private BigDecimal minimumDeposit;
|
||||
private List<DepositMethodItemDto> activeMethods;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class DepositMethodItemDto {
|
||||
private Integer pid;
|
||||
private String name;
|
||||
private String network;
|
||||
private String example;
|
||||
private BigDecimal minDepositSum;
|
||||
}
|
||||
}
|
||||
16
src/main/java/com/honey/honey/dto/ErrorResponse.java
Normal file
16
src/main/java/com/honey/honey/dto/ErrorResponse.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ErrorResponse {
|
||||
private String message;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Request body for 3rd party deposit webhook: POST /api/deposit_webhook/{token}.
|
||||
* usd_amount: decimal, e.g. 1.45 (3rd party sends as number).
|
||||
*/
|
||||
@Data
|
||||
public class ExternalDepositWebhookRequest {
|
||||
|
||||
@JsonProperty("user_id")
|
||||
private Integer userId;
|
||||
|
||||
@JsonProperty("usd_amount")
|
||||
private Double usdAmount;
|
||||
}
|
||||
24
src/main/java/com/honey/honey/dto/JoinRoundRequest.java
Normal file
24
src/main/java/com/honey/honey/dto/JoinRoundRequest.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class JoinRoundRequest {
|
||||
@NotNull(message = "Room number is required")
|
||||
@Min(value = 1, message = "Room number must be between 1 and 3")
|
||||
@Max(value = 3, message = "Room number must be between 1 and 3")
|
||||
private Integer roomNumber;
|
||||
|
||||
@NotNull(message = "Bet amount is required")
|
||||
@Positive(message = "Bet amount must be a positive integer")
|
||||
private Long betAmount;
|
||||
}
|
||||
|
||||
24
src/main/java/com/honey/honey/dto/MessageDto.java
Normal file
24
src/main/java/com/honey/honey/dto/MessageDto.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MessageDto {
|
||||
private Long id;
|
||||
private Long ticketId;
|
||||
private Integer userId;
|
||||
private String message;
|
||||
private Instant createdAt;
|
||||
private Boolean isFromSupport; // true if message is from support agent (different user_id than ticket owner)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class NotifyBroadcastRequest {
|
||||
/** HTML/text message. */
|
||||
private String message;
|
||||
/** Optional image URL (ignored if videoUrl is set). */
|
||||
private String imageUrl;
|
||||
/** Optional video URL (takes priority over imageUrl). */
|
||||
private String videoUrl;
|
||||
/** Internal user id range start (default 1). */
|
||||
private Integer userIdFrom;
|
||||
/** Internal user id range end (default max id). */
|
||||
private Integer userIdTo;
|
||||
/** Optional button text; if set, adds an inline button that opens the mini app. */
|
||||
private String buttonText;
|
||||
/** When true, skip users whose latest notification_audit record has status FAILED (e.g. blocked the bot). When false or null, send to all in range. */
|
||||
private Boolean ignoreBlocked;
|
||||
}
|
||||
23
src/main/java/com/honey/honey/dto/ParticipantDto.java
Normal file
23
src/main/java/com/honey/honey/dto/ParticipantDto.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ParticipantDto {
|
||||
@JsonProperty("uI")
|
||||
private Integer userId;
|
||||
|
||||
@JsonProperty("b")
|
||||
private Long bet; // In tickets (not bigint)
|
||||
|
||||
@JsonProperty("aU")
|
||||
private String avatarUrl;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PaymentInvoiceResponse {
|
||||
private String invoiceId; // Order ID to be used in Telegram invoice
|
||||
private String invoiceUrl; // Invoice URL to open in Telegram (null for crypto)
|
||||
private Integer starsAmount; // Amount in Stars (legacy)
|
||||
private Double usdAmount; // USD as decimal, e.g. 3.25 (crypto)
|
||||
private Long ticketsAmount; // Tickets amount in bigint format
|
||||
}
|
||||
|
||||
16
src/main/java/com/honey/honey/dto/PaymentWebhookRequest.java
Normal file
16
src/main/java/com/honey/honey/dto/PaymentWebhookRequest.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class PaymentWebhookRequest {
|
||||
private String orderId; // Order ID from Telegram invoice
|
||||
private Long telegramUserId; // Telegram user ID
|
||||
private String telegramPaymentChargeId; // Telegram payment charge ID
|
||||
private String telegramProviderPaymentChargeId; // Telegram provider payment charge ID
|
||||
private Integer starsAmount; // Amount in Stars
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
20
src/main/java/com/honey/honey/dto/PayoutHistoryEntryDto.java
Normal file
20
src/main/java/com/honey/honey/dto/PayoutHistoryEntryDto.java
Normal file
@@ -0,0 +1,20 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* DTO for payout history table entries.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PayoutHistoryEntryDto {
|
||||
private Long amount; // Total in bigint format (will be converted to tickets on frontend)
|
||||
private String date; // Formatted as dd.MM at HH:mm (e.g., "13.01 at 22:29")
|
||||
private String status; // PROCESSING, COMPLETED, CANCELLED
|
||||
}
|
||||
|
||||
24
src/main/java/com/honey/honey/dto/PayoutResponse.java
Normal file
24
src/main/java/com/honey/honey/dto/PayoutResponse.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PayoutResponse {
|
||||
private Long id;
|
||||
private String username;
|
||||
private String type;
|
||||
private String giftName;
|
||||
private Long total;
|
||||
private Integer starsAmount;
|
||||
private Integer quantity;
|
||||
private String status;
|
||||
private Long createdAt; // Unix timestamp in milliseconds
|
||||
private Long resolvedAt; // Unix timestamp in milliseconds, null if not resolved
|
||||
}
|
||||
|
||||
29
src/main/java/com/honey/honey/dto/PromotionDetailDto.java
Normal file
29
src/main/java/com/honey/honey/dto/PromotionDetailDto.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PromotionDetailDto {
|
||||
private Integer id;
|
||||
private String type;
|
||||
private String status;
|
||||
private Instant startTime;
|
||||
private Instant endTime;
|
||||
private Long totalReward;
|
||||
private List<PromotionLeaderboardEntryDto> leaderboard;
|
||||
/** 1-based position of current user (0 if not in leaderboard). */
|
||||
private int userPosition;
|
||||
/** Total number of participants. */
|
||||
private int userTotal;
|
||||
private BigDecimal userPoints;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PromotionLeaderboardEntryDto {
|
||||
private int place;
|
||||
private String screenName;
|
||||
private BigDecimal points;
|
||||
/** Reward for this place in tickets (null if no reward for this place). */
|
||||
private Long rewardTickets;
|
||||
}
|
||||
22
src/main/java/com/honey/honey/dto/PromotionListItemDto.java
Normal file
22
src/main/java/com/honey/honey/dto/PromotionListItemDto.java
Normal file
@@ -0,0 +1,22 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PromotionListItemDto {
|
||||
private Integer id;
|
||||
private String type;
|
||||
private String status;
|
||||
private Instant startTime;
|
||||
private Instant endTime;
|
||||
/** Total prize fund in bigint (1 ticket = 1_000_000). */
|
||||
private Long totalReward;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class QuickAnswerCreateRequest {
|
||||
private String text;
|
||||
}
|
||||
|
||||
18
src/main/java/com/honey/honey/dto/QuickAnswerDto.java
Normal file
18
src/main/java/com/honey/honey/dto/QuickAnswerDto.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class QuickAnswerDto {
|
||||
private Integer id;
|
||||
private String text;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
|
||||
25
src/main/java/com/honey/honey/dto/RecentBonusClaimDto.java
Normal file
25
src/main/java/com/honey/honey/dto/RecentBonusClaimDto.java
Normal file
@@ -0,0 +1,25 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* DTO for recent daily bonus claims.
|
||||
* Contains user information and claim timestamp.
|
||||
* The claimedAt field contains the raw timestamp, but the date field contains the formatted string.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class RecentBonusClaimDto {
|
||||
private String avatarUrl;
|
||||
private String screenName;
|
||||
private LocalDateTime claimedAt;
|
||||
private String date; // Formatted date string (dd.MM 'at' HH:mm) with timezone
|
||||
}
|
||||
|
||||
19
src/main/java/com/honey/honey/dto/ReferralDto.java
Normal file
19
src/main/java/com/honey/honey/dto/ReferralDto.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ReferralDto {
|
||||
private String name; // screen_name from db_users_a
|
||||
private Long commission; // to_referer_1/2/3 from db_users_d (bigint, needs to be divided by 1,000,000 on frontend)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
25
src/main/java/com/honey/honey/dto/ReferralLevelDto.java
Normal file
25
src/main/java/com/honey/honey/dto/ReferralLevelDto.java
Normal file
@@ -0,0 +1,25 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ReferralLevelDto {
|
||||
private Integer level; // 1-5
|
||||
private Integer refererId;
|
||||
private Integer referralCount;
|
||||
private Long commissionsEarned;
|
||||
private Long commissionsPaid;
|
||||
/** Commissions earned in USD (converted from tickets: 1000 tickets = 1 USD). */
|
||||
private BigDecimal commissionsEarnedUsd;
|
||||
/** Commissions paid in USD (converted from tickets). */
|
||||
private BigDecimal commissionsPaidUsd;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SupportTicketReplyRequest {
|
||||
@NotBlank(message = "Message is required")
|
||||
private String message;
|
||||
}
|
||||
|
||||
26
src/main/java/com/honey/honey/dto/TaskDto.java
Normal file
26
src/main/java/com/honey/honey/dto/TaskDto.java
Normal file
@@ -0,0 +1,26 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class TaskDto {
|
||||
private Integer id;
|
||||
private String type; // referral, follow, other
|
||||
private Long requirement; // For referral tasks: number of friends. For other tasks: deposit_total threshold in bigint (frontend converts)
|
||||
private Long rewardAmount; // bigint format
|
||||
private String rewardType; // Tickets (all tasks use Tickets as reward type)
|
||||
private String title;
|
||||
private String description;
|
||||
private Integer displayOrder;
|
||||
private Boolean claimed; // Whether user has claimed this task
|
||||
private String progress; // Progress string like "1008 / 30" or "CLAIMED" (for referral) or null (for follow/other - frontend handles)
|
||||
private Long currentValue; // Current value in bigint format (referals1 for referral, depositTotal for other). Frontend converts for display.
|
||||
private String localizedRewardText; // Localized reward text like "+2 Билеты" or "+5 Билетов" (for proper grammar)
|
||||
}
|
||||
|
||||
19
src/main/java/com/honey/honey/dto/TelegramApiResponse.java
Normal file
19
src/main/java/com/honey/honey/dto/TelegramApiResponse.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Telegram Bot API response wrapper.
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class TelegramApiResponse {
|
||||
|
||||
@JsonProperty("ok")
|
||||
private Boolean ok;
|
||||
|
||||
@JsonProperty("description")
|
||||
private String description;
|
||||
}
|
||||
17
src/main/java/com/honey/honey/dto/TelegramSendResult.java
Normal file
17
src/main/java/com/honey/honey/dto/TelegramSendResult.java
Normal file
@@ -0,0 +1,17 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Result of a single Telegram send call for broadcast audit.
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class TelegramSendResult {
|
||||
private boolean success;
|
||||
/** HTTP status code from Telegram API (e.g. 200, 403, 429). */
|
||||
private int statusCode;
|
||||
}
|
||||
26
src/main/java/com/honey/honey/dto/TicketDetailDto.java
Normal file
26
src/main/java/com/honey/honey/dto/TicketDetailDto.java
Normal file
@@ -0,0 +1,26 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import com.honey.honey.model.SupportTicket.TicketStatus;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class TicketDetailDto {
|
||||
private Long id;
|
||||
private String subject;
|
||||
private TicketStatus status;
|
||||
private Instant createdAt;
|
||||
private Instant updatedAt;
|
||||
private List<MessageDto> messages;
|
||||
}
|
||||
|
||||
|
||||
|
||||
25
src/main/java/com/honey/honey/dto/TicketDto.java
Normal file
25
src/main/java/com/honey/honey/dto/TicketDto.java
Normal file
@@ -0,0 +1,25 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import com.honey.honey.model.SupportTicket.TicketStatus;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class TicketDto {
|
||||
private Long id;
|
||||
private String subject;
|
||||
private TicketStatus status;
|
||||
private Instant createdAt;
|
||||
private Instant updatedAt;
|
||||
private Integer messageCount;
|
||||
}
|
||||
|
||||
|
||||
|
||||
39
src/main/java/com/honey/honey/dto/TransactionDto.java
Normal file
39
src/main/java/com/honey/honey/dto/TransactionDto.java
Normal file
@@ -0,0 +1,39 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* DTO for a transaction entry in transaction history.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class TransactionDto {
|
||||
/**
|
||||
* Amount in bigint format (positive for credits, negative for debits).
|
||||
* Example: +900000000 means +900.0000 (credit)
|
||||
* Example: -100000000 means -100.0000 (debit)
|
||||
*/
|
||||
private Long amount;
|
||||
|
||||
/**
|
||||
* Date formatted as dd.MM at HH:mm (e.g., "13.01 at 22:29")
|
||||
*/
|
||||
private String date;
|
||||
|
||||
/**
|
||||
* Transaction type: DEPOSIT, WITHDRAWAL, TASK_BONUS, CANCELLATION_OF_WITHDRAWAL
|
||||
*/
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* Task ID for TASK_BONUS type (null for other types)
|
||||
*/
|
||||
private Integer taskId;
|
||||
}
|
||||
|
||||
|
||||
25
src/main/java/com/honey/honey/dto/UserCheckDto.java
Normal file
25
src/main/java/com/honey/honey/dto/UserCheckDto.java
Normal file
@@ -0,0 +1,25 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* DTO for user check endpoint response.
|
||||
* Contains user information for external applications.
|
||||
* Always returned with HTTP 200; use {@code found} to distinguish user-not-found from success.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UserCheckDto {
|
||||
/** When false, user was not found by telegramId; other fields are null. */
|
||||
private Boolean found;
|
||||
private Integer dateReg;
|
||||
private Double tickets; // balance_a / 1,000,000
|
||||
private Integer depositTotal; // Sum of completed payments stars_amount
|
||||
private Integer refererId; // referer_id_1 from db_users_d
|
||||
}
|
||||
|
||||
23
src/main/java/com/honey/honey/dto/UserDepositDto.java
Normal file
23
src/main/java/com/honey/honey/dto/UserDepositDto.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
|
||||
/** Deposit (payment) row for admin user detail Deposits tab. */
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UserDepositDto {
|
||||
private Long id;
|
||||
private BigDecimal usdAmount;
|
||||
private String status;
|
||||
private String orderId;
|
||||
private Instant createdAt;
|
||||
private Instant completedAt;
|
||||
}
|
||||
26
src/main/java/com/honey/honey/dto/UserDto.java
Normal file
26
src/main/java/com/honey/honey/dto/UserDto.java
Normal file
@@ -0,0 +1,26 @@
|
||||
package com.honey.honey.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UserDto {
|
||||
private Integer id; // User ID
|
||||
private Long telegram_id;
|
||||
private String username;
|
||||
private String screenName; // User's screen name
|
||||
private Integer dateReg; // Registration date (Unix timestamp in seconds)
|
||||
private String ip;
|
||||
private Long balanceA; // Balance (stored as bigint, represents number with 6 decimal places)
|
||||
private String avatarUrl; // Public URL of user's avatar
|
||||
private String languageCode; // User's language preference (EN, RU, DE, IT, NL, PL, FR, ES, ID, TR)
|
||||
private Boolean paymentEnabled; // Runtime toggle: deposits (Store, Payment Options, etc.) allowed
|
||||
private Boolean payoutEnabled; // Runtime toggle: withdrawals (Payout, crypto withdrawal) allowed
|
||||
private Boolean promotionsEnabled; // Runtime toggle: Promotions button and /api/promotions endpoints
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user