Skip to content

Commit 4fb6d64

Browse files
committed
feat: fix stripe webhook data+payment_status, add Mux video system, filter account pending orders
1 parent 38a9a2d commit 4fb6d64

File tree

13 files changed

+1159
-27
lines changed

13 files changed

+1159
-27
lines changed

app/account/page.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,22 +38,26 @@ export default async function AccountPage() {
3838
.eq("id", user.id)
3939
.single();
4040

41-
// Fetch Orders
41+
// Fetch Orders — filter out pending/cancelled so customers only see real orders
4242
const { data: orders } = await supabase
4343
.from("orders")
4444
.select(`
4545
id,
4646
created_at,
4747
amount_total,
4848
status,
49+
payment_status,
4950
tracking_number,
5051
order_items (
5152
quantity,
53+
product_name,
54+
variant_name,
5255
products (title, images),
5356
product_variants (image_url)
5457
)
5558
`)
5659
.eq("customer_email", user.email)
60+
.not("status", "in", '("pending","cancelled")')
5761
.order("created_at", { ascending: false });
5862

5963
return (

app/admin/videos/page.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { createClient } from '@/lib/supabase/admin'
2+
import type { Metadata } from 'next'
3+
import VideoManagerClient from '@/components/admin/VideoManagerClient'
4+
5+
export const dynamic = 'force-dynamic'
6+
export const metadata: Metadata = { title: 'Video Manager | Admin — DINA COSMETIC' }
7+
8+
export default async function VideoManagerPage() {
9+
const supabase = await createClient()
10+
11+
const { data: videos, error } = await supabase
12+
.from('videos')
13+
.select('*')
14+
.order('created_at', { ascending: false })
15+
16+
if (error) {
17+
console.error('[Admin Video Manager] fetch error:', error)
18+
}
19+
20+
return <VideoManagerClient initialVideos={videos ?? []} />
21+
}

app/api/admin/videos/route.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { createClient } from "@supabase/supabase-js";
2+
import { NextRequest, NextResponse } from "next/server";
3+
4+
const supabase = createClient(
5+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
6+
process.env.SUPABASE_SERVICE_ROLE_KEY!
7+
);
8+
9+
/** GET /api/admin/videos — returns all videos ordered by creation date */
10+
export async function GET() {
11+
const { data, error } = await supabase
12+
.from("videos")
13+
.select("*")
14+
.order("created_at", { ascending: false });
15+
16+
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
17+
return NextResponse.json(data ?? []);
18+
}
19+
20+
/** PATCH /api/admin/videos — update video metadata (title, description) */
21+
export async function PATCH(req: NextRequest) {
22+
try {
23+
const { id, title, description } = await req.json();
24+
if (!id) return NextResponse.json({ error: "id required" }, { status: 400 });
25+
26+
const { data, error } = await supabase
27+
.from("videos")
28+
.update({ title, description, updated_at: new Date().toISOString() })
29+
.eq("id", id)
30+
.select()
31+
.single();
32+
33+
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
34+
return NextResponse.json(data);
35+
} catch (err: any) {
36+
return NextResponse.json({ error: err.message }, { status: 500 });
37+
}
38+
}
39+
40+
/** DELETE /api/admin/videos?id=xxx — remove a video from the DB (Mux asset stays unless you also call Mux API) */
41+
export async function DELETE(req: NextRequest) {
42+
const id = req.nextUrl.searchParams.get("id");
43+
if (!id) return NextResponse.json({ error: "id required" }, { status: 400 });
44+
45+
const { error } = await supabase.from("videos").delete().eq("id", id);
46+
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
47+
return NextResponse.json({ success: true });
48+
}

app/api/mux/create-upload/route.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { NextResponse } from "next/server";
2+
import Mux from "@mux/mux-node";
3+
import { createClient } from "@supabase/supabase-js";
4+
5+
const mux = new Mux({
6+
tokenId: process.env.MUX_TOKEN_ID!,
7+
tokenSecret: process.env.MUX_TOKEN_SECRET!,
8+
});
9+
10+
const supabase = createClient(
11+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
12+
process.env.SUPABASE_SERVICE_ROLE_KEY!
13+
);
14+
15+
/**
16+
* POST /api/mux/create-upload
17+
* Creates a Mux Direct Upload and pre-inserts a video row in the DB
18+
* so the admin can track upload progress from the media manager.
19+
* Body: { title?: string }
20+
*/
21+
export async function POST(req: Request) {
22+
try {
23+
const body = await req.json().catch(() => ({}));
24+
const title = body?.title || null;
25+
26+
const upload = await mux.video.uploads.create({
27+
new_asset_settings: {
28+
playback_policy: ["public"],
29+
mp4_support: "standard",
30+
},
31+
cors_origin: process.env.NEXT_PUBLIC_SITE_URL || "*",
32+
});
33+
34+
// Pre-insert video row with upload_id so webhook can link the asset
35+
const { data: video } = await supabase
36+
.from("videos")
37+
.insert({
38+
mux_upload_id: upload.id,
39+
mux_asset_id: "pending", // Mux webhook will update this
40+
mux_playback_id: "pending", // Mux webhook will update this
41+
title: title,
42+
status: "uploading",
43+
})
44+
.select("id")
45+
.single();
46+
47+
return NextResponse.json({
48+
url: upload.url,
49+
uploadId: upload.id,
50+
videoId: video?.id,
51+
});
52+
} catch (err: any) {
53+
console.error("[Mux] create-upload error:", err);
54+
return NextResponse.json({ error: err.message }, { status: 500 });
55+
}
56+
}

app/api/mux/webhook/route.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { NextResponse } from "next/server";
2+
import { createClient } from "@supabase/supabase-js";
3+
4+
const supabase = createClient(
5+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
6+
process.env.SUPABASE_SERVICE_ROLE_KEY!
7+
);
8+
9+
/**
10+
* Mux Webhook handler.
11+
* Listens for video lifecycle events:
12+
* - video.upload.asset_created → insert row with mux_upload_id for tracking
13+
* - video.asset.created → row created, still processing
14+
* - video.asset.ready → asset playback is live, update status + metadata
15+
* - video.asset.errored → mark as errored so admin can retry
16+
*
17+
* We do NOT verify the Mux webhook signature here for simplicity; in production,
18+
* you can add Mux webhook signature verification using the mux-node SDK.
19+
*/
20+
export async function POST(req: Request) {
21+
try {
22+
const body = await req.json();
23+
const type = body.type as string;
24+
const data = body.data as any;
25+
26+
console.log(`[Mux Webhook] Event: ${type}`, { assetId: data?.id });
27+
28+
if (type === "video.upload.asset_created") {
29+
// Upload has been received by Mux, asset creation started
30+
const uploadId = body.object?.id || data?.upload_id;
31+
const assetId = data?.asset_id || data?.id;
32+
33+
if (assetId && uploadId) {
34+
// Update the row that was pre-inserted by the admin UI via upload ID
35+
const { error } = await supabase
36+
.from("videos")
37+
.update({ mux_asset_id: assetId, status: "processing" })
38+
.eq("mux_upload_id", uploadId);
39+
40+
if (error) console.error("[Mux Webhook] upload.asset_created update error:", error);
41+
}
42+
}
43+
44+
if (type === "video.asset.created") {
45+
// Asset record created in Mux — may not have playback_id yet
46+
const playbackId = data?.playback_ids?.[0]?.id ?? null;
47+
const uploadId = data?.upload_id ?? null;
48+
49+
// Try to upsert by mux_asset_id in case a row already exists from upload phase
50+
const { error } = await supabase
51+
.from("videos")
52+
.upsert(
53+
{
54+
mux_asset_id: data.id,
55+
mux_playback_id: playbackId ?? "",
56+
mux_upload_id: uploadId,
57+
status: "processing",
58+
thumbnail_url: playbackId
59+
? `https://image.mux.com/${playbackId}/thumbnail.jpg`
60+
: null,
61+
},
62+
{ onConflict: "mux_asset_id" }
63+
);
64+
65+
if (error) console.error("[Mux Webhook] asset.created upsert error:", error);
66+
}
67+
68+
if (type === "video.asset.ready") {
69+
// Asset is live — update status, playback_id, and metadata
70+
const playbackId = data?.playback_ids?.[0]?.id ?? null;
71+
72+
const { error } = await supabase
73+
.from("videos")
74+
.update({
75+
status: "ready",
76+
mux_playback_id: playbackId ?? "",
77+
thumbnail_url: playbackId
78+
? `https://image.mux.com/${playbackId}/thumbnail.jpg`
79+
: null,
80+
duration: data?.duration ?? null,
81+
aspect_ratio: data?.aspect_ratio ?? null,
82+
updated_at: new Date().toISOString(),
83+
})
84+
.eq("mux_asset_id", data.id);
85+
86+
if (error) console.error("[Mux Webhook] asset.ready update error:", error);
87+
else console.log(`[Mux Webhook] Asset ready: ${data.id} (playback: ${playbackId})`);
88+
}
89+
90+
if (type === "video.asset.errored") {
91+
const { error } = await supabase
92+
.from("videos")
93+
.update({ status: "errored", updated_at: new Date().toISOString() })
94+
.eq("mux_asset_id", data.id);
95+
96+
if (error) console.error("[Mux Webhook] asset.errored update error:", error);
97+
else console.error(`[Mux Webhook] Asset ERRORED: ${data.id}`);
98+
}
99+
100+
return NextResponse.json({ received: true });
101+
} catch (err: any) {
102+
console.error("[Mux Webhook] Unhandled error:", err);
103+
return NextResponse.json({ error: err.message }, { status: 500 });
104+
}
105+
}

app/api/videos/[id]/route.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { createClient } from "@supabase/supabase-js";
2+
import { NextRequest, NextResponse } from "next/server";
3+
4+
const supabase = createClient(
5+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
6+
process.env.SUPABASE_SERVICE_ROLE_KEY!
7+
);
8+
9+
export async function GET(
10+
_req: NextRequest,
11+
{ params }: { params: Promise<{ id: string }> }
12+
) {
13+
const { id } = await params;
14+
15+
const { data, error } = await supabase
16+
.from("videos")
17+
.select("*")
18+
.eq("id", id)
19+
.single();
20+
21+
if (error || !data)
22+
return NextResponse.json({ error: "Video not found" }, { status: 404 });
23+
24+
return NextResponse.json(data);
25+
}

app/api/webhook/stripe/route.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ export async function POST(req: Request) {
4040
return NextResponse.json({ received: true, duplicate: true });
4141
}
4242

43+
// ✅ CRITICAL FIX: Always persist full event payload immediately on receipt
44+
// This ensures we have the raw data even if processing crashes below.
45+
await supabase.from("stripe_events").upsert(
46+
{ id: event.id, type: event.type, data: event.data, processed: false },
47+
{ onConflict: "id" }
48+
);
49+
4350
try {
4451
if (
4552
event.type === "checkout.session.completed" ||
@@ -54,7 +61,7 @@ export async function POST(req: Request) {
5461
`[Stripe Webhook] Session ${session.id} payment_status=${session.payment_status}. Awaiting async_payment_succeeded.`
5562
);
5663
await supabase.from("stripe_events").upsert(
57-
{ id: event.id, type: event.type, processed: true, error: "payment_pending" },
64+
{ id: event.id, type: event.type, data: event.data, processed: true, error: "payment_pending" },
5865
{ onConflict: "id" }
5966
);
6067
return NextResponse.json({ received: true, status: "pending_payment" });
@@ -107,6 +114,7 @@ export async function POST(req: Request) {
107114
.from("orders")
108115
.update({
109116
status: "paid",
117+
payment_status: "paid", // ✅ CRITICAL FIX: was missing
110118
fulfillment_status: "unfulfilled",
111119
customer_email: customer?.email || "",
112120
customer_name: customerName,
@@ -182,9 +190,9 @@ export async function POST(req: Request) {
182190
}
183191
}
184192

185-
// Mark event processed
193+
// Mark event processed (data already stored at top of handler)
186194
await supabase.from("stripe_events").upsert(
187-
{ id: event.id, type: event.type, processed: true },
195+
{ id: event.id, type: event.type, data: event.data, processed: true },
188196
{ onConflict: "id" }
189197
);
190198

@@ -220,7 +228,7 @@ export async function POST(req: Request) {
220228
console.warn(`[Stripe Webhook] Async payment FAILED for order ${orderId}. Marked cancelled.`);
221229
}
222230
await supabase.from("stripe_events").upsert(
223-
{ id: event.id, type: event.type, processed: true },
231+
{ id: event.id, type: event.type, data: event.data, processed: true },
224232
{ onConflict: "id" }
225233
);
226234

@@ -237,14 +245,14 @@ export async function POST(req: Request) {
237245
console.info(`[Stripe Webhook] Session expired for order ${orderId}. Marked cancelled.`);
238246
}
239247
await supabase.from("stripe_events").upsert(
240-
{ id: event.id, type: event.type, processed: true },
248+
{ id: event.id, type: event.type, data: event.data, processed: true },
241249
{ onConflict: "id" }
242250
);
243251

244252
} else {
245-
// All other events — just log
253+
// All other events — just log (data already stored at top)
246254
await supabase.from("stripe_events").upsert(
247-
{ id: event.id, type: event.type, processed: true },
255+
{ id: event.id, type: event.type, data: event.data, processed: true },
248256
{ onConflict: "id" }
249257
);
250258
}
@@ -257,6 +265,7 @@ export async function POST(req: Request) {
257265
await supabase.from("stripe_events").upsert({
258266
id: event.id,
259267
type: event.type,
268+
data: event.data,
260269
processed: false,
261270
error: error.message,
262271
}, { onConflict: 'id' });

0 commit comments

Comments
 (0)