feat(core): Stripe Connect Payouts & Twitter API Integration
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 7s

This commit is contained in:
OG T
2026-06-08 10:27:57 +08:00
parent d03519c71c
commit a6201007aa
6 changed files with 184 additions and 29 deletions

View File

@@ -189,3 +189,58 @@ export async function releasePayment(
},
});
}
export async function executePayout(
tx: Prisma.TransactionClient,
taskId: string,
developerWallet: string,
amount: number,
currency: string,
idempotencyKey: string
) {
const existing = await tx.ledgerEntry.findUnique({
where: { idempotency_key: idempotencyKey },
});
if (existing) {
if (existing.response_status === "SUCCESS") return existing;
throw new Error(`Previous executePayout failed for idempotencyKey: ${idempotencyKey}`);
}
let transferId = `mock_transfer_${taskId}`;
// If Stripe is configured and wallet looks like a Stripe Connect Account
if (stripe && developerWallet.startsWith("acct_") && !ALLOW_MCP_CLAIM_WITHOUT_STRIPE) {
try {
const transfer = await stripe.transfers.create({
amount: amount,
currency: currency.toLowerCase(),
destination: developerWallet,
description: `Bounty Payout for Task ${taskId}`,
}, { idempotencyKey });
transferId = transfer.id;
} catch (error: any) {
await tx.ledgerEntry.create({
data: {
task_id: taskId,
phase: "PAYOUT",
idempotency_key: idempotencyKey,
stripe_object_id: null,
response_status: "FAILED",
http_status: error.statusCode || 500,
},
});
throw error;
}
}
return await tx.ledgerEntry.create({
data: {
task_id: taskId,
phase: "PAYOUT",
idempotency_key: idempotencyKey,
stripe_object_id: transferId,
response_status: "SUCCESS",
http_status: 200,
},
});
}

View File

@@ -1,8 +1,5 @@
/**
* X (Twitter) FOMO Broadcaster
* 用於發送病毒式行銷推文,製造錯失恐懼 (FOMO)。
*/
import { sendTrafficAlert } from "./traffic-alert";
import { TwitterApi } from "twitter-api-v2";
export type FomoEventType = "HIGH_VALUE_BOUNTY" | "SPEED_RUN" | "A2A_SUBCONTRACT";
@@ -31,9 +28,14 @@ function generateTweetText(event: FomoEvent): string {
export async function broadcastFomoEvent(event: FomoEvent) {
const text = generateTweetText(event);
const bearerToken = process.env.TWITTER_BEARER_TOKEN;
// Use App Key/Secret + Access Token/Secret for User Context Posting (OAuth 1.0a)
const appKey = process.env.TWITTER_API_KEY;
const appSecret = process.env.TWITTER_API_SECRET;
const accessToken = process.env.TWITTER_ACCESS_TOKEN;
const accessSecret = process.env.TWITTER_ACCESS_SECRET;
if (!bearerToken) {
if (!appKey || !appSecret || !accessToken || !accessSecret) {
// Mock Mode: Log to console and send to internal traffic alert
console.log(`[X Broadcaster Mock] Would tweet: "${text}"`);
void sendTrafficAlert({
@@ -48,23 +50,16 @@ export async function broadcastFomoEvent(event: FomoEvent) {
}
try {
// 簡單的 Fetch 實作,針對 Twitter V2 API
const response = await fetch("https://api.twitter.com/2/tweets", {
method: "POST",
headers: {
"Authorization": `Bearer ${bearerToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ text }),
const client = new TwitterApi({
appKey,
appSecret,
accessToken,
accessSecret,
});
if (!response.ok) {
const errText = await response.text();
console.error("[X Broadcaster] API Error:", errText);
} else {
console.log("[X Broadcaster] Successfully posted tweet.");
}
const response = await client.v2.tweet(text);
console.log("[X Broadcaster] Successfully posted tweet. Tweet ID:", response.data.id);
} catch (err) {
console.error("[X Broadcaster] Network Error:", err);
console.error("[X Broadcaster] Twitter API Error:", err);
}
}