Implement collaboration wallets, withdrawals, and app UI flows
This commit is contained in:
164
supabase/functions/create-withdrawal/index.ts
Normal file
164
supabase/functions/create-withdrawal/index.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
|
||||
|
||||
const corsHeaders = {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
|
||||
};
|
||||
|
||||
serve(async (req: Request): Promise<Response> => {
|
||||
if (req.method === "OPTIONS") {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
}
|
||||
|
||||
try {
|
||||
const authHeader = req.headers.get("Authorization");
|
||||
if (!authHeader) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Unauthorized" }),
|
||||
{ status: 401, headers: { ...corsHeaders, "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
|
||||
const supabase = createClient(
|
||||
Deno.env.get("SUPABASE_URL")!,
|
||||
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!,
|
||||
{
|
||||
auth: {
|
||||
autoRefreshToken: false,
|
||||
persistSession: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const token = authHeader.replace("Bearer ", "");
|
||||
const { data: authData } = await supabase.auth.getUser(token);
|
||||
const user = authData.user;
|
||||
if (!user) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Unauthorized" }),
|
||||
{ status: 401, headers: { ...corsHeaders, "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
|
||||
const { amount, notes } = await req.json();
|
||||
const parsedAmount = Number(amount || 0);
|
||||
if (parsedAmount <= 0) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Invalid withdrawal amount" }),
|
||||
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
|
||||
const { data: wallet } = await supabase
|
||||
.rpc("get_collaborator_wallet", { p_user_id: user.id });
|
||||
const currentBalance = Number(wallet?.[0]?.current_balance || 0);
|
||||
|
||||
const { data: settings } = await supabase.rpc("get_collaboration_settings");
|
||||
const minWithdrawal = Number(settings?.[0]?.min_withdrawal_amount || 100000);
|
||||
const maxPendingWithdrawals = Number(settings?.[0]?.max_pending_withdrawals || 1);
|
||||
|
||||
if (currentBalance < minWithdrawal) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: `Minimum withdrawal is Rp ${minWithdrawal.toLocaleString("id-ID")}` }),
|
||||
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
|
||||
if (parsedAmount > currentBalance) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Insufficient available balance", available: currentBalance }),
|
||||
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
|
||||
const { data: existingPending } = await supabase
|
||||
.from("withdrawals")
|
||||
.select("id")
|
||||
.eq("user_id", user.id)
|
||||
.eq("status", "pending");
|
||||
|
||||
if ((existingPending?.length || 0) >= maxPendingWithdrawals) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: `Maximum ${maxPendingWithdrawals} pending withdrawal(s) allowed` }),
|
||||
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
|
||||
const { data: profile } = await supabase
|
||||
.from("profiles")
|
||||
.select("bank_account_name, bank_account_number, bank_name")
|
||||
.eq("id", user.id)
|
||||
.maybeSingle();
|
||||
|
||||
if (!profile?.bank_account_number || !profile?.bank_account_name || !profile?.bank_name) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Please complete your bank account information in profile settings" }),
|
||||
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
|
||||
const { data: withdrawal, error: createError } = await supabase
|
||||
.from("withdrawals")
|
||||
.insert({
|
||||
user_id: user.id,
|
||||
amount: parsedAmount,
|
||||
status: "pending",
|
||||
payment_method: "bank_transfer",
|
||||
payment_reference: `${profile.bank_name} - ${profile.bank_account_number} (${profile.bank_account_name})`,
|
||||
notes: notes || null,
|
||||
created_by: user.id,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (createError || !withdrawal) {
|
||||
throw createError || new Error("Failed to create withdrawal");
|
||||
}
|
||||
|
||||
const { data: txId, error: holdError } = await supabase
|
||||
.rpc("hold_withdrawal_amount", {
|
||||
p_user_id: user.id,
|
||||
p_withdrawal_id: withdrawal.id,
|
||||
p_amount: parsedAmount,
|
||||
});
|
||||
|
||||
if (holdError) {
|
||||
await supabase.from("withdrawals").delete().eq("id", withdrawal.id);
|
||||
throw holdError;
|
||||
}
|
||||
|
||||
await supabase
|
||||
.from("withdrawals")
|
||||
.update({ wallet_transaction_id: txId })
|
||||
.eq("id", withdrawal.id);
|
||||
|
||||
await supabase.functions.invoke("send-collaboration-notification", {
|
||||
body: {
|
||||
type: "withdrawal_requested",
|
||||
withdrawalId: withdrawal.id,
|
||||
userId: user.id,
|
||||
amount: parsedAmount,
|
||||
bankInfo: {
|
||||
bankName: profile.bank_name,
|
||||
accountNumber: profile.bank_account_number,
|
||||
accountName: profile.bank_account_name,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
withdrawal: { ...withdrawal, wallet_transaction_id: txId },
|
||||
}),
|
||||
{ status: 201, headers: { ...corsHeaders, "Content-Type": "application/json" } },
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : "Failed to create withdrawal";
|
||||
return new Response(
|
||||
JSON.stringify({ error: message }),
|
||||
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
});
|
||||
50
supabase/functions/get-owner-identity/index.ts
Normal file
50
supabase/functions/get-owner-identity/index.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
|
||||
|
||||
const corsHeaders = {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
|
||||
};
|
||||
|
||||
serve(async (req: Request): Promise<Response> => {
|
||||
if (req.method === "OPTIONS") {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
}
|
||||
|
||||
try {
|
||||
const supabase = createClient(
|
||||
Deno.env.get("SUPABASE_URL")!,
|
||||
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!,
|
||||
{
|
||||
auth: {
|
||||
autoRefreshToken: false,
|
||||
persistSession: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const { data: settings, error } = await supabase
|
||||
.from("platform_settings")
|
||||
.select("owner_name, owner_avatar_url")
|
||||
.limit(1)
|
||||
.maybeSingle();
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
owner_name: settings?.owner_name || "Dwindi",
|
||||
owner_avatar_url: settings?.owner_avatar_url || "",
|
||||
}),
|
||||
{ status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" } },
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : "Failed to get owner identity";
|
||||
return new Response(
|
||||
JSON.stringify({ error: message }),
|
||||
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -38,8 +38,10 @@ serve(async (req: Request): Promise<Response> => {
|
||||
*,
|
||||
profiles(email, name),
|
||||
order_items (
|
||||
id,
|
||||
product_id,
|
||||
product:products (title, type)
|
||||
unit_price,
|
||||
product:products (title, type, collaborator_user_id, profit_share_percentage, auto_grant_access)
|
||||
),
|
||||
consulting_sessions (
|
||||
id,
|
||||
@@ -80,8 +82,16 @@ serve(async (req: Request): Promise<Response> => {
|
||||
const userEmail = order.profiles?.email || "";
|
||||
const userName = order.profiles?.name || userEmail.split('@')[0] || "Pelanggan";
|
||||
const orderItems = order.order_items as Array<{
|
||||
id: string;
|
||||
product_id: string;
|
||||
product: { title: string; type: string };
|
||||
unit_price?: number;
|
||||
product: {
|
||||
title: string;
|
||||
type: string;
|
||||
collaborator_user_id?: string | null;
|
||||
profit_share_percentage?: number | null;
|
||||
auto_grant_access?: boolean | null;
|
||||
};
|
||||
}>;
|
||||
|
||||
// Check if this is a consulting order by checking consulting_sessions
|
||||
@@ -218,6 +228,84 @@ serve(async (req: Request): Promise<Response> => {
|
||||
});
|
||||
console.log("[HANDLE-PAID] Access granted for product:", item.product_id);
|
||||
}
|
||||
|
||||
// Collaboration: credit collaborator wallet if this product has a collaborator
|
||||
const collaboratorUserId = item.product?.collaborator_user_id;
|
||||
const profitSharePct = Number(item.product?.profit_share_percentage || 0);
|
||||
const autoGrantAccess = item.product?.auto_grant_access !== false;
|
||||
const itemPrice = Number(item.unit_price || 0);
|
||||
|
||||
if (collaboratorUserId && profitSharePct > 0 && itemPrice > 0) {
|
||||
const hostShare = itemPrice * ((100 - profitSharePct) / 100);
|
||||
const collaboratorShare = itemPrice * (profitSharePct / 100);
|
||||
|
||||
// Save profit split to order_items
|
||||
const { error: splitError } = await supabase
|
||||
.from("order_items")
|
||||
.update({
|
||||
host_share: hostShare,
|
||||
collaborator_share: collaboratorShare,
|
||||
})
|
||||
.eq("id", item.id);
|
||||
|
||||
if (splitError) {
|
||||
console.error("[HANDLE-PAID] Failed to update order item split:", splitError);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Credit collaborator wallet (also stores wallet_transaction_id on order_items)
|
||||
const { data: transactionId, error: creditError } = await supabase
|
||||
.rpc("credit_collaborator_wallet", {
|
||||
p_user_id: collaboratorUserId,
|
||||
p_order_item_id: item.id,
|
||||
p_amount: collaboratorShare,
|
||||
p_description: `Profit from sale: ${item.product?.title || "Product"}`,
|
||||
});
|
||||
|
||||
if (creditError) {
|
||||
console.error("[HANDLE-PAID] Failed to credit collaborator wallet:", creditError);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[HANDLE-PAID] Credited collaborator wallet: ${collaboratorUserId} + Rp ${collaboratorShare}, tx=${transactionId}`
|
||||
);
|
||||
|
||||
// Grant collaborator access to the same product if enabled
|
||||
if (autoGrantAccess) {
|
||||
const { error: collaboratorAccessError } = await supabase
|
||||
.from("user_access")
|
||||
.upsert(
|
||||
{
|
||||
user_id: collaboratorUserId,
|
||||
product_id: item.product_id,
|
||||
access_type: "collaborator",
|
||||
granted_by: order.user_id,
|
||||
},
|
||||
{ onConflict: "user_id,product_id" }
|
||||
);
|
||||
|
||||
if (collaboratorAccessError) {
|
||||
console.error("[HANDLE-PAID] Failed to grant collaborator access:", collaboratorAccessError);
|
||||
}
|
||||
}
|
||||
|
||||
// Notify collaborator about new sale
|
||||
const { error: collabNotifyError } = await supabase.functions.invoke("send-collaboration-notification", {
|
||||
body: {
|
||||
type: "new_sale",
|
||||
collaboratorUserId,
|
||||
productTitle: item.product?.title || "Product",
|
||||
profitAmount: collaboratorShare,
|
||||
profitSharePercentage: profitSharePct,
|
||||
saleDate: order.created_at,
|
||||
},
|
||||
});
|
||||
|
||||
if (collabNotifyError) {
|
||||
console.error("[HANDLE-PAID] Failed to send collaborator notification:", collabNotifyError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const productTitles = orderItems.map(i => i.product.title);
|
||||
@@ -257,12 +345,13 @@ serve(async (req: Request): Promise<Response> => {
|
||||
{ headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.error("[HANDLE-PAID] Error:", error);
|
||||
const message = error instanceof Error ? error.message : "Internal server error";
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
error: error.message || "Internal server error"
|
||||
error: message
|
||||
}),
|
||||
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
||||
);
|
||||
@@ -271,9 +360,9 @@ serve(async (req: Request): Promise<Response> => {
|
||||
|
||||
// Helper function to send notification
|
||||
async function sendNotification(
|
||||
supabase: any,
|
||||
supabase: ReturnType<typeof createClient>,
|
||||
templateKey: string,
|
||||
data: Record<string, any>
|
||||
data: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
console.log("[HANDLE-PAID] Sending notification:", templateKey);
|
||||
|
||||
@@ -319,8 +408,8 @@ async function sendNotification(
|
||||
},
|
||||
body: JSON.stringify({
|
||||
template_key: templateKey,
|
||||
recipient_email: data.email,
|
||||
recipient_name: data.user_name || data.nama,
|
||||
recipient_email: String(data.email || ""),
|
||||
recipient_name: String((data.user_name as string) || (data.nama as string) || ""),
|
||||
variables: data,
|
||||
}),
|
||||
});
|
||||
|
||||
154
supabase/functions/process-withdrawal/index.ts
Normal file
154
supabase/functions/process-withdrawal/index.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
|
||||
|
||||
const corsHeaders = {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
|
||||
};
|
||||
|
||||
serve(async (req: Request): Promise<Response> => {
|
||||
if (req.method === "OPTIONS") {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
}
|
||||
|
||||
try {
|
||||
const authHeader = req.headers.get("Authorization");
|
||||
if (!authHeader) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Unauthorized" }),
|
||||
{ status: 401, headers: { ...corsHeaders, "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
|
||||
const supabase = createClient(
|
||||
Deno.env.get("SUPABASE_URL")!,
|
||||
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!,
|
||||
{
|
||||
auth: {
|
||||
autoRefreshToken: false,
|
||||
persistSession: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const token = authHeader.replace("Bearer ", "");
|
||||
const { data: authData } = await supabase.auth.getUser(token);
|
||||
const user = authData.user;
|
||||
if (!user) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Unauthorized" }),
|
||||
{ status: 401, headers: { ...corsHeaders, "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
|
||||
const { data: isAdmin } = await supabase
|
||||
.from("user_roles")
|
||||
.select("role")
|
||||
.eq("user_id", user.id)
|
||||
.eq("role", "admin")
|
||||
.maybeSingle();
|
||||
|
||||
if (!isAdmin) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Forbidden - Admin only" }),
|
||||
{ status: 403, headers: { ...corsHeaders, "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
|
||||
const { withdrawalId, status, payment_reference, admin_notes, reason } = await req.json();
|
||||
if (!withdrawalId || !["completed", "rejected"].includes(status)) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Invalid payload" }),
|
||||
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
|
||||
const { data: withdrawal } = await supabase
|
||||
.from("withdrawals")
|
||||
.select("*, user:profiles(name, email)")
|
||||
.eq("id", withdrawalId)
|
||||
.maybeSingle();
|
||||
|
||||
if (!withdrawal) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Withdrawal not found" }),
|
||||
{ status: 404, headers: { ...corsHeaders, "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
|
||||
if (withdrawal.status !== "pending") {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Withdrawal already processed" }),
|
||||
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
|
||||
if (status === "completed") {
|
||||
await supabase.rpc("complete_withdrawal", {
|
||||
p_user_id: withdrawal.user_id,
|
||||
p_withdrawal_id: withdrawalId,
|
||||
p_amount: withdrawal.amount,
|
||||
p_payment_reference: payment_reference || "-",
|
||||
});
|
||||
|
||||
await supabase
|
||||
.from("withdrawals")
|
||||
.update({
|
||||
status: "completed",
|
||||
processed_at: new Date().toISOString(),
|
||||
payment_reference: payment_reference || null,
|
||||
admin_notes: admin_notes || null,
|
||||
updated_by: user.id,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.eq("id", withdrawalId);
|
||||
|
||||
await supabase.functions.invoke("send-collaboration-notification", {
|
||||
body: {
|
||||
type: "withdrawal_completed",
|
||||
userId: withdrawal.user_id,
|
||||
amount: withdrawal.amount,
|
||||
paymentReference: payment_reference || "-",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await supabase.rpc("reject_withdrawal", {
|
||||
p_user_id: withdrawal.user_id,
|
||||
p_withdrawal_id: withdrawalId,
|
||||
p_amount: withdrawal.amount,
|
||||
p_reason: reason || "Withdrawal rejected by admin",
|
||||
});
|
||||
|
||||
await supabase
|
||||
.from("withdrawals")
|
||||
.update({
|
||||
status: "rejected",
|
||||
processed_at: new Date().toISOString(),
|
||||
admin_notes: admin_notes || reason || null,
|
||||
updated_by: user.id,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.eq("id", withdrawalId);
|
||||
|
||||
await supabase.functions.invoke("send-collaboration-notification", {
|
||||
body: {
|
||||
type: "withdrawal_rejected",
|
||||
userId: withdrawal.user_id,
|
||||
amount: withdrawal.amount,
|
||||
reason: admin_notes || reason || "Withdrawal rejected",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ success: true }),
|
||||
{ status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" } },
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : "Failed to process withdrawal";
|
||||
return new Response(
|
||||
JSON.stringify({ error: message }),
|
||||
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
});
|
||||
172
supabase/functions/send-collaboration-notification/index.ts
Normal file
172
supabase/functions/send-collaboration-notification/index.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
|
||||
|
||||
const corsHeaders = {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
|
||||
};
|
||||
|
||||
interface NotificationPayload {
|
||||
type: "new_sale" | "withdrawal_requested" | "withdrawal_completed" | "withdrawal_rejected";
|
||||
collaboratorUserId?: string;
|
||||
userId?: string;
|
||||
amount?: number;
|
||||
productTitle?: string;
|
||||
profitAmount?: number;
|
||||
profitSharePercentage?: number;
|
||||
saleDate?: string;
|
||||
paymentReference?: string;
|
||||
reason?: string;
|
||||
bankInfo?: {
|
||||
bankName: string;
|
||||
accountNumber: string;
|
||||
accountName: string;
|
||||
};
|
||||
}
|
||||
|
||||
async function sendEmail(recipient: string, subject: string, content: string): Promise<void> {
|
||||
const response = await fetch(`${Deno.env.get("SUPABASE_URL")}/functions/v1/send-email-v2`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
recipient,
|
||||
subject,
|
||||
content,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(`send-email-v2 failed: ${response.status} ${text}`);
|
||||
}
|
||||
}
|
||||
|
||||
serve(async (req: Request): Promise<Response> => {
|
||||
if (req.method === "OPTIONS") {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
}
|
||||
|
||||
try {
|
||||
const supabase = createClient(
|
||||
Deno.env.get("SUPABASE_URL")!,
|
||||
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!,
|
||||
{
|
||||
auth: {
|
||||
autoRefreshToken: false,
|
||||
persistSession: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const data = await req.json() as NotificationPayload;
|
||||
const { type } = data;
|
||||
|
||||
let recipientEmail = "";
|
||||
let subject = "";
|
||||
let htmlContent = "";
|
||||
|
||||
if (type === "new_sale") {
|
||||
const { data: collaborator } = await supabase
|
||||
.from("profiles")
|
||||
.select("email, name")
|
||||
.eq("id", data.collaboratorUserId || "")
|
||||
.maybeSingle();
|
||||
|
||||
recipientEmail = collaborator?.email || "";
|
||||
subject = `🎉 You earned Rp ${(data.profitAmount || 0).toLocaleString("id-ID")} from ${data.productTitle || "your product"}!`;
|
||||
htmlContent = `
|
||||
<h2>Great news, ${collaborator?.name || "Partner"}!</h2>
|
||||
<p>Your collaborative webinar <strong>${data.productTitle || "-"}</strong> just made a sale.</p>
|
||||
<ul>
|
||||
<li>Your Share: ${data.profitSharePercentage || 0}%</li>
|
||||
<li>Profit Earned: <strong>Rp ${(data.profitAmount || 0).toLocaleString("id-ID")}</strong></li>
|
||||
<li>Sale Date: ${data.saleDate ? new Date(data.saleDate).toLocaleDateString("id-ID") : "-"}</li>
|
||||
</ul>
|
||||
`;
|
||||
} else if (type === "withdrawal_requested") {
|
||||
const { data: adminRole } = await supabase
|
||||
.from("user_roles")
|
||||
.select("user_id")
|
||||
.eq("role", "admin")
|
||||
.limit(1)
|
||||
.maybeSingle();
|
||||
|
||||
const { data: admin } = await supabase
|
||||
.from("profiles")
|
||||
.select("email")
|
||||
.eq("id", adminRole?.user_id || "")
|
||||
.maybeSingle();
|
||||
|
||||
recipientEmail = admin?.email || "";
|
||||
subject = "💸 New Withdrawal Request";
|
||||
htmlContent = `
|
||||
<h2>New Withdrawal Request</h2>
|
||||
<p>A collaborator has requested withdrawal:</p>
|
||||
<ul>
|
||||
<li>Amount: <strong>Rp ${(data.amount || 0).toLocaleString("id-ID")}</strong></li>
|
||||
<li>Bank: ${data.bankInfo?.bankName || "-"}</li>
|
||||
<li>Account: ${data.bankInfo?.accountNumber || "-"} (${data.bankInfo?.accountName || "-"})</li>
|
||||
</ul>
|
||||
`;
|
||||
} else if (type === "withdrawal_completed") {
|
||||
const { data: user } = await supabase
|
||||
.from("profiles")
|
||||
.select("email, name")
|
||||
.eq("id", data.userId || "")
|
||||
.maybeSingle();
|
||||
|
||||
recipientEmail = user?.email || "";
|
||||
subject = `✅ Withdrawal Completed: Rp ${(data.amount || 0).toLocaleString("id-ID")}`;
|
||||
htmlContent = `
|
||||
<h2>Withdrawal Completed, ${user?.name || "Partner"}!</h2>
|
||||
<ul>
|
||||
<li>Amount: <strong>Rp ${(data.amount || 0).toLocaleString("id-ID")}</strong></li>
|
||||
<li>Payment Reference: ${data.paymentReference || "-"}</li>
|
||||
</ul>
|
||||
`;
|
||||
} else if (type === "withdrawal_rejected") {
|
||||
const { data: user } = await supabase
|
||||
.from("profiles")
|
||||
.select("email, name")
|
||||
.eq("id", data.userId || "")
|
||||
.maybeSingle();
|
||||
|
||||
recipientEmail = user?.email || "";
|
||||
subject = "❌ Withdrawal Request Returned";
|
||||
htmlContent = `
|
||||
<h2>Withdrawal Request Returned</h2>
|
||||
<p>Hi ${user?.name || "Partner"},</p>
|
||||
<p>Your withdrawal request of <strong>Rp ${(data.amount || 0).toLocaleString("id-ID")}</strong> has been returned to your wallet.</p>
|
||||
<p>Reason: ${data.reason || "Contact admin for details"}</p>
|
||||
`;
|
||||
} else {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Unknown notification type" }),
|
||||
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
|
||||
if (!recipientEmail) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: "Recipient email not found" }),
|
||||
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
|
||||
await sendEmail(recipientEmail, subject, htmlContent);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ success: true }),
|
||||
{ status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" } },
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : "Failed to send notification";
|
||||
return new Response(
|
||||
JSON.stringify({ error: message }),
|
||||
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } },
|
||||
);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user