mirror of
https://github.com/AeThex-Corporation/AeThex-OS.git
synced 2026-04-17 22:07:20 +00:00
448 lines
11 KiB
TypeScript
448 lines
11 KiB
TypeScript
import { supabase } from "./supabase";
|
|
import {
|
|
InsertSplitProposal,
|
|
InsertSplitVote,
|
|
SplitProposal,
|
|
SplitVote,
|
|
} from "../shared/schema";
|
|
import { updateRevenueSplit } from "./splits";
|
|
|
|
interface ProposalResult {
|
|
success: boolean;
|
|
proposal_id?: string;
|
|
error?: string;
|
|
}
|
|
|
|
interface VoteResult {
|
|
success: boolean;
|
|
vote_id?: string;
|
|
error?: string;
|
|
}
|
|
|
|
interface VoteOutcomeResult {
|
|
success: boolean;
|
|
approved: boolean;
|
|
total_votes?: number;
|
|
approve_count?: number;
|
|
reject_count?: number;
|
|
message?: string;
|
|
error?: string;
|
|
}
|
|
|
|
/**
|
|
* Create a proposal to change the revenue split rule for a project
|
|
* Only current collaborators can propose splits
|
|
*/
|
|
export async function createSplitProposal(
|
|
input: {
|
|
project_id: string;
|
|
proposed_by: string;
|
|
proposed_rule: Record<string, number>;
|
|
voting_rule: "unanimous" | "majority";
|
|
description?: string;
|
|
expires_at?: Date;
|
|
}
|
|
): Promise<ProposalResult> {
|
|
try {
|
|
// Validate percentages sum to 1.0
|
|
const sum = Object.values(input.proposed_rule).reduce(
|
|
(acc, val) => acc + val,
|
|
0
|
|
);
|
|
if (Math.abs(sum - 1.0) > 0.001) {
|
|
return {
|
|
success: false,
|
|
error: `Percentages must sum to 1.0, got ${sum.toFixed(4)}`,
|
|
};
|
|
}
|
|
|
|
// Check that proposer is a collaborator on the project
|
|
const { data: collaborator, error: collabError } = await supabase
|
|
.from("project_collaborators")
|
|
.select("*")
|
|
.eq("project_id", input.project_id)
|
|
.eq("user_id", input.proposed_by)
|
|
.maybeSingle();
|
|
|
|
if (collabError) {
|
|
return {
|
|
success: false,
|
|
error: `Failed to check collaborator status: ${collabError.message}`,
|
|
};
|
|
}
|
|
|
|
if (!collaborator) {
|
|
return {
|
|
success: false,
|
|
error: "Only project collaborators can propose split changes",
|
|
};
|
|
}
|
|
|
|
const expiresAt = input.expires_at || new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
|
|
|
|
const { data, error } = await supabase
|
|
.from("split_proposals")
|
|
.insert({
|
|
project_id: input.project_id,
|
|
proposed_by: input.proposed_by,
|
|
proposed_rule: input.proposed_rule,
|
|
voting_rule: input.voting_rule,
|
|
description: input.description,
|
|
expires_at: expiresAt,
|
|
proposal_status: "pending",
|
|
})
|
|
.select("id")
|
|
.single();
|
|
|
|
if (error) {
|
|
return {
|
|
success: false,
|
|
error: `Failed to create proposal: ${error.message}`,
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
proposal_id: data.id,
|
|
};
|
|
} catch (err: any) {
|
|
return {
|
|
success: false,
|
|
error: `Unexpected error: ${err.message}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cast a vote on a split proposal
|
|
* Only project collaborators can vote
|
|
*/
|
|
export async function castVote(
|
|
input: {
|
|
proposal_id: string;
|
|
voter_id: string;
|
|
vote: "approve" | "reject";
|
|
reason?: string;
|
|
}
|
|
): Promise<VoteResult> {
|
|
try {
|
|
// Fetch the proposal to verify it exists and get project_id
|
|
const { data: proposal, error: proposalError } = await supabase
|
|
.from("split_proposals")
|
|
.select("*")
|
|
.eq("id", input.proposal_id)
|
|
.maybeSingle();
|
|
|
|
if (proposalError) {
|
|
return {
|
|
success: false,
|
|
error: `Failed to fetch proposal: ${proposalError.message}`,
|
|
};
|
|
}
|
|
|
|
if (!proposal) {
|
|
return {
|
|
success: false,
|
|
error: "Proposal not found",
|
|
};
|
|
}
|
|
|
|
// Check if proposal is still open (not expired)
|
|
if (proposal.expires_at && new Date(proposal.expires_at) < new Date()) {
|
|
return {
|
|
success: false,
|
|
error: "Proposal voting period has expired",
|
|
};
|
|
}
|
|
|
|
// Check if voter is a collaborator
|
|
const { data: collaborator, error: collabError } = await supabase
|
|
.from("project_collaborators")
|
|
.select("*")
|
|
.eq("project_id", proposal.project_id)
|
|
.eq("user_id", input.voter_id)
|
|
.maybeSingle();
|
|
|
|
if (collabError) {
|
|
return {
|
|
success: false,
|
|
error: `Failed to check voter status: ${collabError.message}`,
|
|
};
|
|
}
|
|
|
|
if (!collaborator) {
|
|
return {
|
|
success: false,
|
|
error: "Only project collaborators can vote",
|
|
};
|
|
}
|
|
|
|
// Check if voter has already voted
|
|
const { data: existingVote, error: checkError } = await supabase
|
|
.from("split_votes")
|
|
.select("*")
|
|
.eq("proposal_id", input.proposal_id)
|
|
.eq("voter_id", input.voter_id)
|
|
.maybeSingle();
|
|
|
|
if (checkError) {
|
|
return {
|
|
success: false,
|
|
error: `Failed to check existing vote: ${checkError.message}`,
|
|
};
|
|
}
|
|
|
|
if (existingVote) {
|
|
return {
|
|
success: false,
|
|
error: "You have already voted on this proposal",
|
|
};
|
|
}
|
|
|
|
// Insert the vote
|
|
const { data, error } = await supabase
|
|
.from("split_votes")
|
|
.insert({
|
|
proposal_id: input.proposal_id,
|
|
voter_id: input.voter_id,
|
|
vote: input.vote,
|
|
reason: input.reason,
|
|
})
|
|
.select("id")
|
|
.single();
|
|
|
|
if (error) {
|
|
return {
|
|
success: false,
|
|
error: `Failed to record vote: ${error.message}`,
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
vote_id: data.id,
|
|
};
|
|
} catch (err: any) {
|
|
return {
|
|
success: false,
|
|
error: `Unexpected error: ${err.message}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a proposal has reached consensus and apply the split if approved
|
|
*/
|
|
export async function evaluateProposal(
|
|
proposalId: string,
|
|
requester_user_id: string
|
|
): Promise<VoteOutcomeResult> {
|
|
try {
|
|
// Fetch proposal
|
|
const { data: proposal, error: proposalError } = await supabase
|
|
.from("split_proposals")
|
|
.select("*")
|
|
.eq("id", proposalId)
|
|
.maybeSingle();
|
|
|
|
if (proposalError) {
|
|
return {
|
|
success: false,
|
|
approved: false,
|
|
error: `Failed to fetch proposal: ${proposalError.message}`,
|
|
};
|
|
}
|
|
|
|
if (!proposal) {
|
|
return {
|
|
success: false,
|
|
approved: false,
|
|
error: "Proposal not found",
|
|
};
|
|
}
|
|
|
|
// If already voted on, return current status
|
|
if (proposal.proposal_status !== "pending") {
|
|
return {
|
|
success: true,
|
|
approved: proposal.proposal_status === "approved",
|
|
message: `Proposal already ${proposal.proposal_status}`,
|
|
};
|
|
}
|
|
|
|
// Get all votes
|
|
const { data: votes, error: votesError } = await supabase
|
|
.from("split_votes")
|
|
.select("*")
|
|
.eq("proposal_id", proposalId);
|
|
|
|
if (votesError) {
|
|
return {
|
|
success: false,
|
|
approved: false,
|
|
error: `Failed to fetch votes: ${votesError.message}`,
|
|
};
|
|
}
|
|
|
|
// Get all collaborators (eligible voters)
|
|
const { data: collaborators, error: collabError } = await supabase
|
|
.from("project_collaborators")
|
|
.select("*")
|
|
.eq("project_id", proposal.project_id);
|
|
|
|
if (collabError) {
|
|
return {
|
|
success: false,
|
|
approved: false,
|
|
error: `Failed to fetch collaborators: ${collabError.message}`,
|
|
};
|
|
}
|
|
|
|
const totalEligible = collaborators?.length || 1;
|
|
const approveCount = votes?.filter((v: any) => v.vote === "approve").length || 0;
|
|
const rejectCount = votes?.filter((v: any) => v.vote === "reject").length || 0;
|
|
const totalVotes = approveCount + rejectCount;
|
|
|
|
// Determine if proposal is approved based on voting rule
|
|
let approved = false;
|
|
|
|
if (proposal.voting_rule === "unanimous") {
|
|
// All eligible voters must approve, OR all votes cast must be approve
|
|
approved = totalVotes > 0 && rejectCount === 0 && approveCount === totalEligible;
|
|
} else {
|
|
// Majority: > 50% of eligible voters approve
|
|
approved = approveCount > totalEligible / 2;
|
|
}
|
|
|
|
// Update proposal status
|
|
const newStatus = approved ? "approved" : "rejected";
|
|
const { error: updateError } = await supabase
|
|
.from("split_proposals")
|
|
.update({ proposal_status: newStatus })
|
|
.eq("id", proposalId);
|
|
|
|
if (updateError) {
|
|
return {
|
|
success: false,
|
|
approved: false,
|
|
error: `Failed to update proposal status: ${updateError.message}`,
|
|
};
|
|
}
|
|
|
|
// If approved, apply the split
|
|
if (approved) {
|
|
// Get the current split version
|
|
const { data: currentSplit, error: splitError } = await supabase
|
|
.from("revenue_splits")
|
|
.select("split_version")
|
|
.eq("project_id", proposal.project_id)
|
|
.is("active_until", null)
|
|
.maybeSingle();
|
|
|
|
if (splitError) {
|
|
return {
|
|
success: true,
|
|
approved: true,
|
|
total_votes: totalVotes,
|
|
approve_count: approveCount,
|
|
reject_count: rejectCount,
|
|
message: "Proposal approved but failed to apply split (no previous version)",
|
|
};
|
|
}
|
|
|
|
const nextVersion = (currentSplit?.split_version || 0) + 1;
|
|
|
|
// Apply the new split rule
|
|
const splitResult = await updateRevenueSplit(
|
|
proposal.project_id,
|
|
{
|
|
split_version: nextVersion,
|
|
rule: proposal.proposed_rule,
|
|
created_by: requester_user_id,
|
|
},
|
|
requester_user_id
|
|
);
|
|
|
|
if (!splitResult.success) {
|
|
return {
|
|
success: true,
|
|
approved: true,
|
|
total_votes: totalVotes,
|
|
approve_count: approveCount,
|
|
reject_count: rejectCount,
|
|
message: `Proposal approved but failed to apply split: ${splitResult.error}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
approved,
|
|
total_votes: totalVotes,
|
|
approve_count: approveCount,
|
|
reject_count: rejectCount,
|
|
message: approved
|
|
? `Proposal approved! Applied new split rule version ${(currentSplit?.split_version || 0) + 1}`
|
|
: `Proposal rejected. Approvals: ${approveCount}, Rejections: ${rejectCount}, Required: ${proposal.voting_rule === "unanimous" ? totalEligible : Math.ceil(totalEligible / 2)}`,
|
|
};
|
|
} catch (err: any) {
|
|
return {
|
|
success: false,
|
|
approved: false,
|
|
error: `Unexpected error: ${err.message}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get proposal details with vote counts
|
|
*/
|
|
export async function getProposalWithVotes(proposalId: string) {
|
|
try {
|
|
const { data: proposal, error: proposalError } = await supabase
|
|
.from("split_proposals")
|
|
.select("*")
|
|
.eq("id", proposalId)
|
|
.maybeSingle();
|
|
|
|
if (proposalError) {
|
|
return { success: false, error: proposalError.message };
|
|
}
|
|
|
|
if (!proposal) {
|
|
return { success: false, error: "Proposal not found" };
|
|
}
|
|
|
|
const { data: votes, error: votesError } = await supabase
|
|
.from("split_votes")
|
|
.select("*")
|
|
.eq("proposal_id", proposalId);
|
|
|
|
if (votesError) {
|
|
return { success: false, error: votesError.message };
|
|
}
|
|
|
|
const { data: collaborators } = await supabase
|
|
.from("project_collaborators")
|
|
.select("*")
|
|
.eq("project_id", proposal.project_id);
|
|
|
|
const approveCount = votes?.filter((v: any) => v.vote === "approve").length || 0;
|
|
const rejectCount = votes?.filter((v: any) => v.vote === "reject").length || 0;
|
|
const totalEligible = collaborators?.length || 0;
|
|
|
|
return {
|
|
success: true,
|
|
proposal,
|
|
votes,
|
|
stats: {
|
|
approve_count: approveCount,
|
|
reject_count: rejectCount,
|
|
total_votes: approveCount + rejectCount,
|
|
total_eligible: totalEligible,
|
|
},
|
|
};
|
|
} catch (err: any) {
|
|
return { success: false, error: err.message };
|
|
}
|
|
}
|