@@ -61,6 +61,14 @@ const resetAllUsageLimits = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const resetUserUsageLimits = (userId: string) => {
|
||||
return apiRequest<ApiResponse<null>>({
|
||||
url: `/admin/usage-limits/reset/${userId}`,
|
||||
client: "admin",
|
||||
method: "post",
|
||||
});
|
||||
};
|
||||
|
||||
// Users
|
||||
const getAllUsers = (params?: AdminPaginationParams) => {
|
||||
return apiRequest<ApiResponse<PaginatedData<AdminUserDto>>>({
|
||||
@@ -119,6 +127,7 @@ export const adminService = {
|
||||
updateSetting,
|
||||
getAllUsageLimits,
|
||||
resetAllUsageLimits,
|
||||
resetUserUsageLimits,
|
||||
getAllUsers,
|
||||
getUserById,
|
||||
deleteUser,
|
||||
|
||||
@@ -49,6 +49,7 @@ export interface AdminUserDto {
|
||||
role?: string;
|
||||
isActive: boolean;
|
||||
subscription?: string;
|
||||
subscriptionStatus?: string;
|
||||
createdAt: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -58,7 +59,7 @@ export interface UpdateUserRoleDto {
|
||||
}
|
||||
|
||||
export interface UpdateUserSubscriptionDto {
|
||||
subscription: string;
|
||||
plan: string;
|
||||
}
|
||||
|
||||
// ========================
|
||||
|
||||
@@ -66,6 +66,18 @@ export const useResetAllUsageLimits = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useResetUserUsageLimits = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (userId: string) => adminService.resetUserUsageLimits(userId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: AdminQueryKeys.usageLimits() });
|
||||
queryClient.invalidateQueries({ queryKey: AdminQueryKeys.users() });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Users
|
||||
export const useAdminUsers = (
|
||||
params?: AdminPaginationParams,
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { apiRequest } from "@/lib/api/api-service";
|
||||
import { ApiResponse } from "@/types/api-response";
|
||||
import type { AnalyzeMatchesDto, AnalysisResultDto, AnalysisHistoryDto } from "./types";
|
||||
import type {
|
||||
AnalyzeMatchesDto,
|
||||
AnalysisResultDto,
|
||||
AnalysisHistoryDto,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* Analysis Service
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { clientMap } from '@/lib/api/client-map';
|
||||
import { Method } from 'axios';
|
||||
import { clientMap } from "@/lib/api/client-map";
|
||||
import { Method } from "axios";
|
||||
|
||||
interface ApiRequestOptions {
|
||||
url: string;
|
||||
@@ -9,14 +9,21 @@ interface ApiRequestOptions {
|
||||
params?: Record<string, string | number | boolean | undefined> | object;
|
||||
}
|
||||
|
||||
export async function apiRequest<T = unknown>(options: ApiRequestOptions): Promise<T> {
|
||||
const { url, client, method = 'get', data, params } = options;
|
||||
export async function apiRequest<T = unknown>(
|
||||
options: ApiRequestOptions,
|
||||
): Promise<T> {
|
||||
const { url, client, method = "get", data, params } = options;
|
||||
const clientInstance = clientMap[client];
|
||||
|
||||
if (!url || !clientInstance) {
|
||||
throw new Error(`Invalid API request: ${client} - ${url}`);
|
||||
}
|
||||
|
||||
const response = await clientInstance.request<T>({ method, url, data, params });
|
||||
const response = await clientInstance.request<T>({
|
||||
method,
|
||||
url,
|
||||
data,
|
||||
params,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { apiRequest } from "@/lib/api/api-service";
|
||||
import { ApiResponse } from "@/types/api-response";
|
||||
import {
|
||||
LoginDto,
|
||||
AuthResponse,
|
||||
RegisterDto,
|
||||
RefreshTokenDto,
|
||||
} from "./types";
|
||||
import { LoginDto, AuthResponse, RegisterDto, RefreshTokenDto } from "./types";
|
||||
|
||||
const login = (data: LoginDto) => {
|
||||
return apiRequest<ApiResponse<AuthResponse>>({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import baseUrl from '@/config/base-url';
|
||||
import { createApiClient } from '@/lib/api/create-api-client';
|
||||
import { AxiosInstance } from 'axios';
|
||||
import baseUrl from "@/config/base-url";
|
||||
import { createApiClient } from "@/lib/api/create-api-client";
|
||||
import { AxiosInstance } from "axios";
|
||||
|
||||
export const clientMap: Record<keyof typeof baseUrl, AxiosInstance> = {
|
||||
auth: createApiClient(baseUrl.auth!),
|
||||
|
||||
@@ -90,4 +90,3 @@ export const couponsService = {
|
||||
suggestCoupon,
|
||||
generateFrequencyCoupon,
|
||||
};
|
||||
|
||||
|
||||
@@ -63,4 +63,3 @@ export const useGenerateFrequencyCoupon = () => {
|
||||
couponsService.generateFrequencyCoupon(dto),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -171,10 +171,10 @@ export interface OddsBandEntryDto {
|
||||
odd_rate?: number;
|
||||
even_rate?: number;
|
||||
"1x_rate"?: number;
|
||||
"x2_rate"?: number;
|
||||
x2_rate?: number;
|
||||
"12_rate"?: number;
|
||||
"1x_sample"?: number;
|
||||
"x2_sample"?: number;
|
||||
x2_sample?: number;
|
||||
"12_sample"?: number;
|
||||
sample: number;
|
||||
[key: string]: unknown;
|
||||
@@ -208,9 +208,15 @@ export interface TripleValueEntryDto {
|
||||
}
|
||||
|
||||
export type HtftComboKey =
|
||||
| "11" | "1x" | "12"
|
||||
| "x1" | "xx" | "x2"
|
||||
| "21" | "2x" | "22";
|
||||
| "11"
|
||||
| "1x"
|
||||
| "12"
|
||||
| "x1"
|
||||
| "xx"
|
||||
| "x2"
|
||||
| "21"
|
||||
| "2x"
|
||||
| "22";
|
||||
|
||||
export interface V27EngineDto {
|
||||
version: string;
|
||||
|
||||
@@ -17,7 +17,7 @@ export const usePrediction = (matchId: string) => {
|
||||
return useQuery({
|
||||
queryKey: PredictionsQueryKeys.detail(matchId),
|
||||
queryFn: () => predictionsService.getPrediction(matchId),
|
||||
enabled: !!matchId,
|
||||
enabled: false, // DO NOT auto-fetch since it consumes limits
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -55,13 +55,15 @@ export const useSyncBulletin = () => {
|
||||
|
||||
export const useGenerateColumns = () => {
|
||||
return useMutation({
|
||||
mutationFn: (dto: GenerateColumnsDto) => sporTotoService.generateColumns(dto),
|
||||
mutationFn: (dto: GenerateColumnsDto) =>
|
||||
sporTotoService.generateColumns(dto),
|
||||
});
|
||||
};
|
||||
|
||||
export const useEvaluateColumns = () => {
|
||||
return useMutation({
|
||||
mutationFn: (dto: EvaluateColumnsDto) => sporTotoService.evaluateColumns(dto),
|
||||
mutationFn: (dto: EvaluateColumnsDto) =>
|
||||
sporTotoService.evaluateColumns(dto),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
export { subscriptionService } from "./service";
|
||||
export {
|
||||
useSubscriptionPlans,
|
||||
useMySubscription,
|
||||
useCheckoutConfig,
|
||||
useCancelSubscription,
|
||||
SubscriptionQueryKeys,
|
||||
} from "./use-hooks";
|
||||
export type {
|
||||
PlanType,
|
||||
BillingInterval,
|
||||
PlanInfo,
|
||||
SubscriptionDto,
|
||||
UsageLimitDto,
|
||||
CheckoutConfigDto,
|
||||
CreateCheckoutRequest,
|
||||
CancelSubscriptionRequest,
|
||||
} from "./types";
|
||||
export {
|
||||
PlanType as PlanTypeEnum,
|
||||
BillingInterval as BillingIntervalEnum,
|
||||
} from "./types";
|
||||
@@ -0,0 +1,59 @@
|
||||
import { apiRequest } from "@/lib/api/api-service";
|
||||
import type { ApiResponse } from "@/types/api-response";
|
||||
import type {
|
||||
PlanInfo,
|
||||
SubscriptionDto,
|
||||
CheckoutConfigDto,
|
||||
CreateCheckoutRequest,
|
||||
CancelSubscriptionRequest,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* Subscriptions Service
|
||||
* Backend: /api/subscriptions/*
|
||||
*/
|
||||
|
||||
const getPlans = (): Promise<ApiResponse<readonly PlanInfo[]>> => {
|
||||
return apiRequest<ApiResponse<readonly PlanInfo[]>>({
|
||||
url: "/subscriptions/plans",
|
||||
client: "core",
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
const getMySubscription = (): Promise<ApiResponse<SubscriptionDto | null>> => {
|
||||
return apiRequest<ApiResponse<SubscriptionDto | null>>({
|
||||
url: "/subscriptions/me",
|
||||
client: "core",
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
const getCheckoutConfig = (
|
||||
dto: CreateCheckoutRequest,
|
||||
): Promise<ApiResponse<CheckoutConfigDto>> => {
|
||||
return apiRequest<ApiResponse<CheckoutConfigDto>>({
|
||||
url: "/subscriptions/checkout",
|
||||
client: "core",
|
||||
method: "post",
|
||||
data: dto,
|
||||
});
|
||||
};
|
||||
|
||||
const cancelSubscription = (
|
||||
dto: CancelSubscriptionRequest,
|
||||
): Promise<ApiResponse<null>> => {
|
||||
return apiRequest<ApiResponse<null>>({
|
||||
url: "/subscriptions/cancel",
|
||||
client: "core",
|
||||
method: "post",
|
||||
data: dto,
|
||||
});
|
||||
};
|
||||
|
||||
export const subscriptionService = {
|
||||
getPlans,
|
||||
getMySubscription,
|
||||
getCheckoutConfig,
|
||||
cancelSubscription,
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
// ========================
|
||||
// Enums — mirrors backend PlanType / BillingIntervalType
|
||||
// ========================
|
||||
|
||||
export enum PlanType {
|
||||
FREE = "free",
|
||||
PLUS = "plus",
|
||||
PREMIUM = "premium",
|
||||
}
|
||||
|
||||
export enum BillingInterval {
|
||||
MONTHLY = "monthly",
|
||||
YEARLY = "yearly",
|
||||
}
|
||||
|
||||
// ========================
|
||||
// Response DTOs
|
||||
// ========================
|
||||
|
||||
export interface UsageLimitDto {
|
||||
analysisCount: number;
|
||||
couponCount: number;
|
||||
maxAnalyses: number;
|
||||
maxCoupons: number;
|
||||
}
|
||||
|
||||
export interface SubscriptionDto {
|
||||
id: string;
|
||||
plan: PlanType;
|
||||
billingInterval: BillingInterval | null;
|
||||
currentPeriodStart: string | null;
|
||||
currentPeriodEnd: string | null;
|
||||
cancelledAt: string | null;
|
||||
cancelEffectiveDate: string | null;
|
||||
paddlePriceId: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface PlanLimits {
|
||||
maxAnalyses: number;
|
||||
maxCoupons: number;
|
||||
}
|
||||
|
||||
export interface PlanInfo {
|
||||
id: PlanType;
|
||||
name: string;
|
||||
description: string;
|
||||
monthlyPrice: number;
|
||||
yearlyPrice: number;
|
||||
currency: string;
|
||||
features: string[];
|
||||
limits: PlanLimits;
|
||||
highlighted: boolean;
|
||||
}
|
||||
|
||||
// ========================
|
||||
// Request DTOs
|
||||
// ========================
|
||||
|
||||
export interface CreateCheckoutRequest {
|
||||
plan: PlanType;
|
||||
billingInterval: BillingInterval;
|
||||
}
|
||||
|
||||
export interface CancelSubscriptionRequest {
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
// ========================
|
||||
// Checkout Config Response
|
||||
// ========================
|
||||
|
||||
export interface CheckoutConfigDto {
|
||||
priceId: string;
|
||||
clientToken: string;
|
||||
environment: string;
|
||||
userId: string;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { subscriptionService } from "./service";
|
||||
import type { CreateCheckoutRequest, CancelSubscriptionRequest } from "./types";
|
||||
|
||||
export const SubscriptionQueryKeys = {
|
||||
all: ["subscriptions"] as const,
|
||||
plans: () => [...SubscriptionQueryKeys.all, "plans"] as const,
|
||||
me: () => [...SubscriptionQueryKeys.all, "me"] as const,
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch all available plans (public, no auth required)
|
||||
*/
|
||||
export const useSubscriptionPlans = () => {
|
||||
return useQuery({
|
||||
queryKey: SubscriptionQueryKeys.plans(),
|
||||
queryFn: () => subscriptionService.getPlans(),
|
||||
staleTime: 1000 * 60 * 30, // Plans rarely change — 30 min cache
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch current user subscription status
|
||||
*/
|
||||
export const useMySubscription = (enabled = true) => {
|
||||
return useQuery({
|
||||
queryKey: SubscriptionQueryKeys.me(),
|
||||
queryFn: () => subscriptionService.getMySubscription(),
|
||||
enabled,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get Paddle checkout config for a plan
|
||||
*/
|
||||
export const useCheckoutConfig = () => {
|
||||
return useMutation({
|
||||
mutationFn: (dto: CreateCheckoutRequest) =>
|
||||
subscriptionService.getCheckoutConfig(dto),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Cancel the current subscription
|
||||
*/
|
||||
export const useCancelSubscription = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (dto: CancelSubscriptionRequest) =>
|
||||
subscriptionService.cancelSubscription(dto),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: SubscriptionQueryKeys.me(),
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -16,6 +16,14 @@ export interface ChangePasswordDto {
|
||||
newPassword: string;
|
||||
}
|
||||
|
||||
export interface UsageLimitDto {
|
||||
analysisCount: number;
|
||||
couponCount: number;
|
||||
maxAnalyses: number;
|
||||
maxCoupons: number;
|
||||
lastResetDate: string;
|
||||
}
|
||||
|
||||
export interface UserResponseDto {
|
||||
id: string;
|
||||
email: string;
|
||||
@@ -25,6 +33,7 @@ export interface UserResponseDto {
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
usageLimit?: UsageLimitDto;
|
||||
}
|
||||
|
||||
const getMe = () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { usersService } from "./service";
|
||||
import type { UpdateProfileDto, ChangePasswordDto } from "./service";
|
||||
|
||||
@@ -7,6 +7,14 @@ export const UsersQueryKeys = {
|
||||
me: () => [...UsersQueryKeys.all, "me"] as const,
|
||||
};
|
||||
|
||||
export const useGetMe = (enabled: boolean = true) => {
|
||||
return useQuery({
|
||||
queryKey: UsersQueryKeys.me(),
|
||||
queryFn: () => usersService.getMe(),
|
||||
enabled,
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateProfile = () => {
|
||||
return useMutation({
|
||||
mutationFn: (dto: UpdateProfileDto) => usersService.updateMe(dto),
|
||||
|
||||
Reference in New Issue
Block a user