mirror of
https://github.com/AeThex-Corporation/AeThex-OS.git
synced 2026-04-18 14:27:20 +00:00
662 lines
15 KiB
TypeScript
662 lines
15 KiB
TypeScript
import { supabase } from "./supabase";
|
|
import { toDecimalString } from "./revenue";
|
|
import {
|
|
InsertPayoutRequest,
|
|
InsertPayout,
|
|
InsertPayoutMethod,
|
|
} from "../shared/schema";
|
|
|
|
interface PayoutRequestResult {
|
|
success: boolean;
|
|
request_id?: string;
|
|
error?: string;
|
|
}
|
|
|
|
interface PayoutResult {
|
|
success: boolean;
|
|
payout_id?: string;
|
|
error?: string;
|
|
}
|
|
|
|
interface EscrowResult {
|
|
success: boolean;
|
|
balance?: string;
|
|
held?: string;
|
|
released?: string;
|
|
error?: string;
|
|
}
|
|
|
|
/**
|
|
* Get user's escrow balance for a specific project
|
|
*/
|
|
export async function getEscrowBalance(
|
|
userId: string,
|
|
projectId: string
|
|
): Promise<EscrowResult> {
|
|
try {
|
|
const { data, error } = await supabase
|
|
.from("escrow_accounts")
|
|
.select("*")
|
|
.eq("user_id", userId)
|
|
.eq("project_id", projectId)
|
|
.maybeSingle();
|
|
|
|
if (error) {
|
|
return {
|
|
success: false,
|
|
error: `Failed to fetch escrow balance: ${error.message}`,
|
|
};
|
|
}
|
|
|
|
if (!data) {
|
|
return {
|
|
success: true,
|
|
balance: "0.00",
|
|
held: "0.00",
|
|
released: "0.00",
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
balance: data.balance,
|
|
held: data.held_amount,
|
|
released: data.released_amount,
|
|
};
|
|
} catch (err: any) {
|
|
return {
|
|
success: false,
|
|
error: `Unexpected error: ${err.message}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add allocated revenue to user's escrow account
|
|
* Called after split allocations are recorded
|
|
*/
|
|
export async function depositToEscrow(
|
|
userId: string,
|
|
projectId: string,
|
|
amount: string // Pre-formatted with toDecimalString
|
|
): Promise<{ success: boolean; error?: string }> {
|
|
try {
|
|
// Check if escrow account exists
|
|
const { data: existing, error: checkError } = await supabase
|
|
.from("escrow_accounts")
|
|
.select("*")
|
|
.eq("user_id", userId)
|
|
.eq("project_id", projectId)
|
|
.maybeSingle();
|
|
|
|
if (checkError) {
|
|
return {
|
|
success: false,
|
|
error: `Failed to check escrow: ${checkError.message}`,
|
|
};
|
|
}
|
|
|
|
if (existing) {
|
|
// Update balance
|
|
const currentBalance = parseFloat(existing.balance);
|
|
const depositAmount = parseFloat(amount);
|
|
const newBalance = currentBalance + depositAmount;
|
|
|
|
const { error: updateError } = await supabase
|
|
.from("escrow_accounts")
|
|
.update({
|
|
balance: toDecimalString(newBalance),
|
|
last_updated: new Date(),
|
|
})
|
|
.eq("id", existing.id);
|
|
|
|
if (updateError) {
|
|
return {
|
|
success: false,
|
|
error: `Failed to update escrow: ${updateError.message}`,
|
|
};
|
|
}
|
|
} else {
|
|
// Create new escrow account
|
|
const { error: insertError } = await supabase
|
|
.from("escrow_accounts")
|
|
.insert({
|
|
user_id: userId,
|
|
project_id: projectId,
|
|
balance: amount,
|
|
held_amount: "0.00",
|
|
released_amount: "0.00",
|
|
});
|
|
|
|
if (insertError) {
|
|
return {
|
|
success: false,
|
|
error: `Failed to create escrow: ${insertError.message}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
return { success: true };
|
|
} catch (err: any) {
|
|
return {
|
|
success: false,
|
|
error: `Unexpected error: ${err.message}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a payout request (user initiates)
|
|
*/
|
|
export async function createPayoutRequest(
|
|
input: {
|
|
user_id: string;
|
|
escrow_account_id: string;
|
|
request_amount: string;
|
|
reason?: string;
|
|
}
|
|
): Promise<PayoutRequestResult> {
|
|
try {
|
|
// Verify escrow account exists and belongs to user
|
|
const { data: escrow, error: escrowError } = await supabase
|
|
.from("escrow_accounts")
|
|
.select("*")
|
|
.eq("id", input.escrow_account_id)
|
|
.eq("user_id", input.user_id)
|
|
.maybeSingle();
|
|
|
|
if (escrowError) {
|
|
return {
|
|
success: false,
|
|
error: `Failed to verify escrow: ${escrowError.message}`,
|
|
};
|
|
}
|
|
|
|
if (!escrow) {
|
|
return {
|
|
success: false,
|
|
error: "Escrow account not found or does not belong to you",
|
|
};
|
|
}
|
|
|
|
// Verify sufficient balance
|
|
const escrowBalance = parseFloat(escrow.balance);
|
|
const requestAmount = parseFloat(input.request_amount);
|
|
|
|
if (requestAmount > escrowBalance) {
|
|
return {
|
|
success: false,
|
|
error: `Insufficient balance. Available: $${escrowBalance.toFixed(2)}, Requested: $${requestAmount.toFixed(2)}`,
|
|
};
|
|
}
|
|
|
|
if (requestAmount <= 0) {
|
|
return {
|
|
success: false,
|
|
error: "Request amount must be greater than 0",
|
|
};
|
|
}
|
|
|
|
// Create payout request with 30-day expiration
|
|
const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
|
|
|
|
const { data, error } = await supabase
|
|
.from("payout_requests")
|
|
.insert({
|
|
user_id: input.user_id,
|
|
escrow_account_id: input.escrow_account_id,
|
|
request_amount: input.request_amount,
|
|
reason: input.reason,
|
|
expires_at: expiresAt,
|
|
status: "pending",
|
|
})
|
|
.select("id")
|
|
.single();
|
|
|
|
if (error) {
|
|
return {
|
|
success: false,
|
|
error: `Failed to create payout request: ${error.message}`,
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
request_id: data.id,
|
|
};
|
|
} catch (err: any) {
|
|
return {
|
|
success: false,
|
|
error: `Unexpected error: ${err.message}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Approve or reject a payout request (admin)
|
|
*/
|
|
export async function reviewPayoutRequest(
|
|
requestId: string,
|
|
approved: boolean,
|
|
notes?: string
|
|
): Promise<{ success: boolean; error?: string }> {
|
|
try {
|
|
const newStatus = approved ? "approved" : "rejected";
|
|
|
|
const { error } = await supabase
|
|
.from("payout_requests")
|
|
.update({
|
|
status: newStatus,
|
|
notes,
|
|
})
|
|
.eq("id", requestId);
|
|
|
|
if (error) {
|
|
return {
|
|
success: false,
|
|
error: `Failed to update payout request: ${error.message}`,
|
|
};
|
|
}
|
|
|
|
return { success: true };
|
|
} catch (err: any) {
|
|
return {
|
|
success: false,
|
|
error: `Unexpected error: ${err.message}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Register or update a payout method for a user
|
|
*/
|
|
export async function registerPayoutMethod(
|
|
input: {
|
|
user_id: string;
|
|
method_type: "stripe_connect" | "paypal" | "bank_transfer" | "crypto";
|
|
metadata: Record<string, any>;
|
|
is_primary?: boolean;
|
|
}
|
|
): Promise<{ success: boolean; method_id?: string; error?: string }> {
|
|
try {
|
|
if (!input.metadata || Object.keys(input.metadata).length === 0) {
|
|
return {
|
|
success: false,
|
|
error: "Metadata required for payout method",
|
|
};
|
|
}
|
|
|
|
const { data, error } = await supabase
|
|
.from("payout_methods")
|
|
.insert({
|
|
user_id: input.user_id,
|
|
method_type: input.method_type,
|
|
metadata: input.metadata,
|
|
is_primary: input.is_primary || false,
|
|
verified: false,
|
|
})
|
|
.select("id")
|
|
.single();
|
|
|
|
if (error) {
|
|
return {
|
|
success: false,
|
|
error: `Failed to register payout method: ${error.message}`,
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
method_id: data.id,
|
|
};
|
|
} catch (err: any) {
|
|
return {
|
|
success: false,
|
|
error: `Unexpected error: ${err.message}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Process a payout (admin/system action)
|
|
* Creates payout record and updates escrow
|
|
*/
|
|
export async function processPayout(
|
|
input: {
|
|
payout_request_id?: string;
|
|
user_id: string;
|
|
escrow_account_id: string;
|
|
payout_method_id: string;
|
|
amount: string;
|
|
}
|
|
): Promise<PayoutResult> {
|
|
try {
|
|
// Verify payout method exists
|
|
const { data: method, error: methodError } = await supabase
|
|
.from("payout_methods")
|
|
.select("*")
|
|
.eq("id", input.payout_method_id)
|
|
.eq("user_id", input.user_id)
|
|
.maybeSingle();
|
|
|
|
if (methodError) {
|
|
return {
|
|
success: false,
|
|
error: `Failed to verify payout method: ${methodError.message}`,
|
|
};
|
|
}
|
|
|
|
if (!method) {
|
|
return {
|
|
success: false,
|
|
error: "Payout method not found or does not belong to you",
|
|
};
|
|
}
|
|
|
|
// Create payout record
|
|
const { data: payout, error: payoutError } = await supabase
|
|
.from("payouts")
|
|
.insert({
|
|
payout_request_id: input.payout_request_id,
|
|
user_id: input.user_id,
|
|
escrow_account_id: input.escrow_account_id,
|
|
payout_method_id: input.payout_method_id,
|
|
amount: input.amount,
|
|
currency: "USD",
|
|
status: "processing",
|
|
})
|
|
.select("id")
|
|
.single();
|
|
|
|
if (payoutError) {
|
|
return {
|
|
success: false,
|
|
error: `Failed to create payout: ${payoutError.message}`,
|
|
};
|
|
}
|
|
|
|
// Update escrow: move from balance to held_amount
|
|
const { data: escrow, error: escrowCheckError } = await supabase
|
|
.from("escrow_accounts")
|
|
.select("*")
|
|
.eq("id", input.escrow_account_id)
|
|
.maybeSingle();
|
|
|
|
if (escrowCheckError) {
|
|
return {
|
|
success: false,
|
|
error: `Failed to verify escrow: ${escrowCheckError.message}`,
|
|
};
|
|
}
|
|
|
|
if (!escrow) {
|
|
return {
|
|
success: false,
|
|
error: "Escrow account not found",
|
|
};
|
|
}
|
|
|
|
const currentBalance = parseFloat(escrow.balance);
|
|
const payoutAmount = parseFloat(input.amount);
|
|
const newBalance = currentBalance - payoutAmount;
|
|
const newHeld = parseFloat(escrow.held_amount) + payoutAmount;
|
|
|
|
if (newBalance < 0) {
|
|
return {
|
|
success: false,
|
|
error: "Insufficient escrow balance",
|
|
};
|
|
}
|
|
|
|
const { error: updateError } = await supabase
|
|
.from("escrow_accounts")
|
|
.update({
|
|
balance: toDecimalString(newBalance),
|
|
held_amount: toDecimalString(newHeld),
|
|
last_updated: new Date(),
|
|
})
|
|
.eq("id", input.escrow_account_id);
|
|
|
|
if (updateError) {
|
|
return {
|
|
success: false,
|
|
error: `Failed to update escrow: ${updateError.message}`,
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
payout_id: payout.id,
|
|
};
|
|
} catch (err: any) {
|
|
return {
|
|
success: false,
|
|
error: `Unexpected error: ${err.message}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mark payout as completed (after external payment confirms)
|
|
*/
|
|
export async function completePayout(
|
|
payoutId: string,
|
|
externalTransactionId?: string
|
|
): Promise<{ success: boolean; error?: string }> {
|
|
try {
|
|
// Fetch payout to update escrow
|
|
const { data: payout, error: fetchError } = await supabase
|
|
.from("payouts")
|
|
.select("*")
|
|
.eq("id", payoutId)
|
|
.maybeSingle();
|
|
|
|
if (fetchError) {
|
|
return {
|
|
success: false,
|
|
error: `Failed to fetch payout: ${fetchError.message}`,
|
|
};
|
|
}
|
|
|
|
if (!payout) {
|
|
return {
|
|
success: false,
|
|
error: "Payout not found",
|
|
};
|
|
}
|
|
|
|
// Update payout status
|
|
const { error: updatePayoutError } = await supabase
|
|
.from("payouts")
|
|
.update({
|
|
status: "completed",
|
|
external_transaction_id: externalTransactionId,
|
|
completed_at: new Date(),
|
|
processed_at: new Date(),
|
|
})
|
|
.eq("id", payoutId);
|
|
|
|
if (updatePayoutError) {
|
|
return {
|
|
success: false,
|
|
error: `Failed to update payout: ${updatePayoutError.message}`,
|
|
};
|
|
}
|
|
|
|
// Update escrow: move from held to released
|
|
const { data: escrow, error: escrowError } = await supabase
|
|
.from("escrow_accounts")
|
|
.select("*")
|
|
.eq("id", payout.escrow_account_id)
|
|
.maybeSingle();
|
|
|
|
if (escrowError) {
|
|
return {
|
|
success: false,
|
|
error: `Failed to fetch escrow: ${escrowError.message}`,
|
|
};
|
|
}
|
|
|
|
if (!escrow) {
|
|
return {
|
|
success: false,
|
|
error: "Escrow account not found",
|
|
};
|
|
}
|
|
|
|
const payoutAmount = parseFloat(payout.amount);
|
|
const newHeld = parseFloat(escrow.held_amount) - payoutAmount;
|
|
const newReleased = parseFloat(escrow.released_amount) + payoutAmount;
|
|
|
|
const { error: updateEscrowError } = await supabase
|
|
.from("escrow_accounts")
|
|
.update({
|
|
held_amount: toDecimalString(newHeld),
|
|
released_amount: toDecimalString(newReleased),
|
|
last_updated: new Date(),
|
|
})
|
|
.eq("id", payout.escrow_account_id);
|
|
|
|
if (updateEscrowError) {
|
|
return {
|
|
success: false,
|
|
error: `Failed to update escrow: ${updateEscrowError.message}`,
|
|
};
|
|
}
|
|
|
|
return { success: true };
|
|
} catch (err: any) {
|
|
return {
|
|
success: false,
|
|
error: `Unexpected error: ${err.message}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mark payout as failed
|
|
*/
|
|
export async function failPayout(
|
|
payoutId: string,
|
|
failureReason?: string
|
|
): Promise<{ success: boolean; error?: string }> {
|
|
try {
|
|
// Fetch payout to restore escrow
|
|
const { data: payout, error: fetchError } = await supabase
|
|
.from("payouts")
|
|
.select("*")
|
|
.eq("id", payoutId)
|
|
.maybeSingle();
|
|
|
|
if (fetchError) {
|
|
return {
|
|
success: false,
|
|
error: `Failed to fetch payout: ${fetchError.message}`,
|
|
};
|
|
}
|
|
|
|
if (!payout) {
|
|
return {
|
|
success: false,
|
|
error: "Payout not found",
|
|
};
|
|
}
|
|
|
|
// Update payout status
|
|
const { error: updatePayoutError } = await supabase
|
|
.from("payouts")
|
|
.update({
|
|
status: "failed",
|
|
failure_reason: failureReason,
|
|
processed_at: new Date(),
|
|
})
|
|
.eq("id", payoutId);
|
|
|
|
if (updatePayoutError) {
|
|
return {
|
|
success: false,
|
|
error: `Failed to update payout: ${updatePayoutError.message}`,
|
|
};
|
|
}
|
|
|
|
// Restore balance: move from held back to balance
|
|
const { data: escrow, error: escrowError } = await supabase
|
|
.from("escrow_accounts")
|
|
.select("*")
|
|
.eq("id", payout.escrow_account_id)
|
|
.maybeSingle();
|
|
|
|
if (escrowError) {
|
|
return {
|
|
success: false,
|
|
error: `Failed to fetch escrow: ${escrowError.message}`,
|
|
};
|
|
}
|
|
|
|
if (!escrow) {
|
|
return {
|
|
success: false,
|
|
error: "Escrow account not found",
|
|
};
|
|
}
|
|
|
|
const payoutAmount = parseFloat(payout.amount);
|
|
const newBalance = parseFloat(escrow.balance) + payoutAmount;
|
|
const newHeld = parseFloat(escrow.held_amount) - payoutAmount;
|
|
|
|
const { error: updateEscrowError } = await supabase
|
|
.from("escrow_accounts")
|
|
.update({
|
|
balance: toDecimalString(newBalance),
|
|
held_amount: toDecimalString(newHeld),
|
|
last_updated: new Date(),
|
|
})
|
|
.eq("id", payout.escrow_account_id);
|
|
|
|
if (updateEscrowError) {
|
|
return {
|
|
success: false,
|
|
error: `Failed to update escrow: ${updateEscrowError.message}`,
|
|
};
|
|
}
|
|
|
|
return { success: true };
|
|
} catch (err: any) {
|
|
return {
|
|
success: false,
|
|
error: `Unexpected error: ${err.message}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get user's payout history
|
|
*/
|
|
export async function getPayoutHistory(userId: string, limit = 50) {
|
|
try {
|
|
const { data, error } = await supabase
|
|
.from("payouts")
|
|
.select("*")
|
|
.eq("user_id", userId)
|
|
.order("created_at", { ascending: false })
|
|
.limit(limit);
|
|
|
|
if (error) {
|
|
return {
|
|
success: false,
|
|
error: `Failed to fetch payouts: ${error.message}`,
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
payouts: data || [],
|
|
count: data?.length || 0,
|
|
};
|
|
} catch (err: any) {
|
|
return {
|
|
success: false,
|
|
error: `Unexpected error: ${err.message}`,
|
|
};
|
|
}
|
|
}
|