| import { supabase } from './client'; |
| import { logger } from '../utils/logger'; |
| import type { TurnitinJob } from './jobs'; |
|
|
| export interface CreateJobParams { |
| userId: string; |
| assignmentTargetId: string; |
| mode: string; |
| filters: Record<string, unknown>; |
| inputFileName: string; |
| inputStoragePath: string; |
| inputFileSize?: number; |
| inputFileSha256?: string; |
| submissionRequestId: string; |
| } |
|
|
| export interface CreateJobResult { |
| jobId: string; |
| created: boolean; |
| } |
|
|
| export interface UserProfile { |
| id: string; |
| email: string; |
| display_name: string | null; |
| ticket_balance: number; |
| role: string; |
| created_at: string; |
| updated_at: string; |
| } |
|
|
| |
| |
| |
| |
| |
| export async function createJobWithTicket(params: CreateJobParams): Promise<CreateJobResult> { |
| const { data, error } = await supabase.rpc('create_job_with_ticket_idempotent', { |
| p_user_id: params.userId, |
| p_assignment_target_id: params.assignmentTargetId, |
| p_mode: params.mode, |
| p_filters: params.filters, |
| p_input_file_name: params.inputFileName, |
| p_input_file_path: params.inputStoragePath, |
| p_input_file_size: params.inputFileSize ?? null, |
| p_input_file_sha256: params.inputFileSha256 ?? null, |
| p_submission_request_id: params.submissionRequestId, |
| }); |
|
|
| if (error) { |
| logger.error('Failed to create job with ticket', { error: error.message, userId: params.userId }); |
| throw error; |
| } |
|
|
| const row = Array.isArray(data) ? data[0] : data; |
| if (!row?.job_id) { |
| throw new Error('Idempotent job creation returned no job ID'); |
| } |
|
|
| return { |
| jobId: row.job_id as string, |
| created: row.created === true, |
| }; |
| } |
|
|
| export async function getJobBySubmissionRequestId( |
| userId: string, |
| submissionRequestId: string, |
| ): Promise<TurnitinJob | null> { |
| const { data, error } = await supabase |
| .from('turnitin_jobs') |
| .select('*') |
| .eq('user_id', userId) |
| .eq('submission_request_id', submissionRequestId) |
| .maybeSingle(); |
|
|
| if (error) { |
| logger.error('Failed to find job by submission request ID', { |
| userId, |
| error: error.message, |
| }); |
| throw error; |
| } |
|
|
| return data as TurnitinJob | null; |
| } |
|
|
| |
| |
| |
| export async function getUserProfile(userId: string): Promise<UserProfile | null> { |
| const { data, error } = await supabase |
| .from('user_profiles') |
| .select('*') |
| .eq('id', userId) |
| .single(); |
|
|
| if (error) { |
| if (error.code === 'PGRST116') return null; |
| logger.error('Failed to get user profile', { userId, error: error.message }); |
| throw error; |
| } |
|
|
| return data as UserProfile; |
| } |
|
|
| |
| |
| |
| |
| export async function adminTopupTickets( |
| adminId: string, |
| targetUserId: string, |
| amount: number, |
| ): Promise<number> { |
| const { data, error } = await supabase.rpc('admin_topup_tickets', { |
| p_admin_id: adminId, |
| p_target_user_id: targetUserId, |
| p_amount: amount, |
| }); |
|
|
| if (error) { |
| logger.error('Failed to top up tickets', { |
| adminId, |
| targetUserId, |
| amount, |
| error: error.message, |
| }); |
| throw error; |
| } |
|
|
| return data as number; |
| } |
|
|
| |
| |
| |
| |
| |
| export async function cancelJob(userId: string, jobId: string): Promise<boolean> { |
| const { data, error } = await supabase.rpc('cancel_job', { |
| p_user_id: userId, |
| p_job_id: jobId, |
| }); |
|
|
| if (error) { |
| logger.error('Failed to cancel job', { userId, jobId, error: error.message }); |
| throw error; |
| } |
|
|
| return data as boolean; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export async function refundFailedJob(jobId: string, reason: string): Promise<boolean> { |
| const { data, error } = await supabase.rpc('refund_failed_job', { |
| p_job_id: jobId, |
| p_reason: reason, |
| }); |
|
|
| if (error) { |
| logger.error('Failed to refund failed job', { |
| jobId, |
| reason, |
| error: error.message, |
| }); |
| throw error; |
| } |
|
|
| return data as boolean; |
| } |
|
|