i have this : /* eslint-disable @typescript-eslint/no-explicit-any */ import { db } from "@/utils/firebase"; import { doc, getDoc, collection, query, where, getDocs, addDoc, serverTimestamp, updateDoc, deleteDoc } from "firebase/firestore"; import { type NextRequest, NextResponse } from "next/server"; import { verifyAuth } from "@/utils/auth-middleware"; import Stripe from "stripe"; import { ChargilyClient } from "@chargily/chargily-pay"; // Initialize Chargily client const chargily = new ChargilyClient({ api_key: process.env.CHARGILY_PAY_SECRET_KEY!, mode: "test", }); // Initialize Stripe const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: "2025-02-24.acacia", }); export async function POST(req: NextRequest) { try { const authCheck = await verifyAuth(); if (!authCheck.success) { return NextResponse.json({ error: authCheck.error }, { status: authCheck.status }); } const userId = authCheck.userId; const { paymentMethod } = await req.json(); if (!paymentMethod) { return NextResponse.json({ error: "Payment method is required" }, { status: 400 }); } // Fetch user's cart const cartQuery = query(collection(db, "carts"), where("userId", "==", userId)); const cartSnapshot = await getDocs(cartQuery); if (cartSnapshot.empty) { return NextResponse.json({ error: "Cart is empty" }, { status: 400 }); } const cartDoc = cartSnapshot.docs[0]; const cartData = cartDoc.data(); if (!cartData.courseIds || cartData.courseIds.length === 0) { return NextResponse.json({ error: "Cart is empty" }, { status: 400 }); } // Fetch course details let totalAmount = 0; const courses = []; for (const courseId of cartData.courseIds) { const courseDoc = await getDoc(doc(db, "courses", courseId)); if (courseDoc.exists()) { const courseData = courseDoc.data(); courses.push({ id: courseDoc.id, title: courseData.title, price: courseData.price, image: courseData.thumbnail || "/placeholder.svg", }); totalAmount += courseData.price; } } // Fetch user details const userDoc = await getDoc(doc(db, "users", `${userId}`)); if (!userDoc.exists()) { return NextResponse.json({ error: "User not found" }, { status: 404 }); } const userData = userDoc.data(); // Validate totalAmount if (totalAmount <= 0) { return NextResponse.json({ error: "Invalid total amount" }, { status: 400 }); } // Create order const orderData = { userId, courses: courses.map((course) => ({ courseId: course.id, title: course.title, price: course.price, })), totalAmount, paymentMethod, status: "pending", paymentStatus: "pending", createdAt: serverTimestamp(), }; const orderRef = await addDoc(collection(db, "orders"), orderData); const orderId = orderRef.id; const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || `${req.headers.get("x-forwarded-proto") || "http"}://${req.headers.get("host")}`; if (!baseUrl.startsWith("http")) { throw new Error(`Invalid base URL: ${baseUrl}`); } if (paymentMethod === "stripe") { const session = await stripe.checkout.sessions.create({ payment_method_types: ["card"], line_items: courses.map((course) => ({ price_data: { currency: "usd", product_data: { name: course.title, images: [new URL(course.image, baseUrl).toString()], }, unit_amount: Math.round(course.price * 100), }, quantity: 1, })), mode: "payment", success_url: `${baseUrl}/checkout/success?session_id={CHECKOUT_SESSION_ID}&order_id=${orderId}`, cancel_url: `${baseUrl}/cart`, client_reference_id: orderId, customer_email: userData.email || undefined, metadata: { orderId, userId }, }); await updateDoc(doc(db, "orders", orderId), { paymentSessionId: session.id, paymentProvider: "stripe", }); await deleteDoc(doc(db, "carts", `${cartData.uid}`)); return NextResponse.json({ message: "Checkout session created", orderId, redirectUrl: session.url, }); } else if (paymentMethod === "chargily") { if (totalAmount < 0.55) { return NextResponse.json({ error: "Minimum payment is $0.55 (75 DZD)" }, { status: 400 }); } const amountInDzd = Math.max(75, Math.round(totalAmount * 135)); const checkout = await chargily.createCheckout({ amount: amountInDzd, currency: "DZD", success_url: `${baseUrl}/checkout/success?order_id=${orderId}`, failure_url: `${baseUrl}/checkout/failure?order_id=${orderId}`, webhook_url: `${baseUrl}/api/webhooks/chargily`, payment_method: "edahabia", metadata: JSON.stringify({ order_id: orderId, user_id: userId }), customer: { name: `${userData.firstName || ""} ${userData.lastName || ""}`.trim() || "Customer", email: userData.email, phone: userData.phone || "Unknown", }, description: `Payment for ${courses.length} course(s)`.slice(0, 255), }); await updateDoc(doc(db, "orders", orderId), { paymentSessionId: checkout.id, paymentProvider: "chargily", paymentUrl: checkout.url, currency: "DZD", convertedAmount: amountInDzd, updatedAt: serverTimestamp(), }); return NextResponse.json({ message: "Chargily payment initiated", orderId, redirectUrl: checkout.url, }); } return NextResponse.json({ error: "Invalid payment method" }, { status: 400 }); } catch (error) { console.error("Error creating checkout session:", error); return NextResponse.json({ error: "Internal Server Error", details: error instanceof Error ? error.message : String(error) }, { status: 500 }); } } and this : and this : /* eslint-disable @typescript-eslint/no-explicit-any */ "use client" import { Button } from "@/components/ui/button" import { CheckCircle } from "lucide-react" import Link from "next/link" import { motion } from "framer-motion" import { useSearchParams } from "next/navigation" import { useEffect, useState, Suspense } from "react" // Animation variants const fadeUp = { hidden: { opacity: 0, y: 20 }, visible: { opacity: 1, y: 0, transition: { duration: 0.6 } }, } function CheckoutSuccessContent() { const searchParams = useSearchParams() const sessionId = searchParams.get("session_id") const orderId = searchParams.get("order_id") const [orderDetails, setOrderDetails] = useState<any>(null) const [loading, setLoading] = useState(true) useEffect(() => { async function fetchOrderDetails() { if (!orderId) return try { // Fetch order details from your API const response = await fetch(`/api/payments/status/${orderId}`) if (!response.ok) { throw new Error("Failed to fetch order details") } const paymentStatus = await response.json() // Fetch the order details const orderResponse = await fetch(`/api/orders/${orderId}`) if (!orderResponse.ok) { throw new Error("Failed to fetch order") } const orderData = await orderResponse.json() setOrderDetails({ id: orderId, date: new Date().toLocaleDateString(), paymentStatus: paymentStatus.paymentStatus, items: orderData.courses || [], total: orderData.totalAmount || 0, }) } catch (error) { console.error("Error fetching order details:", error) // Fallback to dummy data if API fails setOrderDetails({ id: orderId || "ORD-12345", date: new Date().toLocaleDateString(), paymentStatus: "completed", items: [ { title: "THE DOCUMENTARY YOU NEED", price: 426, }, ], total: 426, }) } finally { setLoading(false) } } fetchOrderDetails() }, [orderId, sessionId]) return ( <main className="bg-black text-white min-h-screen"> {/* Header Section */} <section className="pt-24 pb-12 md:pt-32 md:pb-16 px-4"> <div className="max-w-6xl mx-auto"> <motion.div className="flex flex-col items-center text-center" initial="hidden" animate="visible" variants={fadeUp} > <div className="w-20 h-20 bg-green-900/30 rounded-full flex items-center justify-center mb-6"> <CheckCircle className="w-10 h-10 text-green-500" /> </div> <motion.h1 className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl mb-4" variants={fadeUp}> Payment Successful! </motion.h1> <motion.p className="text-xl text-gray-300 max-w-2xl" variants={fadeUp}> Thank you for your purchase. Your order has been processed successfully and you now have access to your courses. </motion.p> </motion.div> </div> </section> {/* Order Details */} <section className="pb-24 px-4"> <div className="max-w-2xl mx-auto"> <motion.div className="bg-gray-900 rounded-xl p-8" initial="hidden" animate="visible" variants={fadeUp}> {loading ? ( <div className="flex justify-center py-8"> <div className="w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin"></div> </div> ) : ( <> <h2 className="text-2xl font-bold mb-6">Order Summary</h2> <div className="space-y-4 mb-6"> <div className="flex justify-between"> <span className="text-gray-400">Order ID:</span> <span>{orderDetails.id}</span> </div> <div className="flex justify-between"> <span className="text-gray-400">Date:</span> <span>{orderDetails.date}</span> </div> <div className="flex justify-between"> <span className="text-gray-400">Status:</span> <span className="text-green-500"> {orderDetails.paymentStatus === "completed" ? "Completed" : "Processing"} </span> </div> </div> <div className="border-t border-gray-800 pt-6 mb-6"> <h3 className="font-medium mb-4">Items</h3> {orderDetails.items.map((item: any, index: number) => ( <div key={index} className="flex justify-between mb-2"> <span>{item.title}</span> <span>${item.price}</span> </div> ))} <div className="border-t border-gray-800 mt-4 pt-4 flex justify-between font-bold"> <span>Total</span> <span>${orderDetails.total}</span> </div> </div> <div className="flex flex-col sm:flex-row gap-4"> <Link href="/my-courses" className="flex-1"> <Button className="w-full bg-primary text-black border-primary hover:bg-primary/90"> GO TO MY COURSES </Button> </Link> <Link href="/" className="flex-1"> <Button variant="outline" className="w-full"> BACK TO HOME </Button> </Link> </div> </> )} </motion.div> </div> </section> </main> ) } export default function CheckoutSuccessPage() { return ( <Suspense fallback={<div>Loading...</div>}> <CheckoutSuccessContent /> </Suspense> ) } i want when the checkout success full the cart will deleted and push the courses to the user and make this : import { db } from "@/utils/firebase" import { doc, getDoc, collection, query, where, getDocs } from "firebase/firestore" import { NextResponse } from "next/server" import { verifyAuth } from "@/utils/auth-middleware" export async function GET() { try { // Verify user is authenticated const authCheck = await verifyAuth() if (!authCheck.success) { return NextResponse.json({ error: authCheck.error }, { status: authCheck.status }) } const userId = authCheck.userId // Get user document to check purchased courses const userDoc = await getDoc(doc(db, "users", `${userId}`)) if (!userDoc.exists()) { return NextResponse.json({ error: "User not found" }, { status: 404 }) } const userData = userDoc.data() const purchasedCourses = userData.purchasedCourses || [] // If no purchased courses, return empty array if (purchasedCourses.length === 0) { return NextResponse.json([]) } // Get course details for each purchased course const courses = [] for (const courseId of purchasedCourses) { const courseDoc = await getDoc(doc(db, "courses", courseId)) if (courseDoc.exists()) { const courseData = courseDoc.data() // Get enrollment status const enrollmentQuery = query( collection(db, "enrollments"), where("userId", "==", userId), where("courseId", "==", courseId), ) const enrollmentSnapshot = await getDocs(enrollmentQuery) let enrollmentStatus = "not_started" let progress = 0 if (!enrollmentSnapshot.empty) { const enrollmentData = enrollmentSnapshot.docs[0].data() enrollmentStatus = enrollmentData.status progress = enrollmentData.progress || 0 } courses.push({ id: courseDoc.id, title: courseData.title, description: courseData.description, image_url: courseData.image_url, category: courseData.category, format: courseData.format, access: courseData.access, completion: courseData.completion, what_include: courseData.what_include, status: enrollmentStatus, progress: progress, purchasedAt: enrollmentSnapshot.empty ? null : enrollmentSnapshot.docs[0].data().enrolledAt, }) } } return NextResponse.json(courses) } catch (error) { console.error("Error fetching user courses:", error) return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }) } } work make it corect bcz its in api/user/courses
I'll help you implement the functionality to delete the cart and add purchased courses to the user when checkout is successful. Let's create a webhook handler to process successful payments and update the user's purchased courses.
I've implemented a complete solution to handle the checkout process and ensure that when a payment is successful:
Here's what each file does:
Webhook Handlers (app/api/webhooks/stripe/route.ts
and app/api/webhooks/chargily/route.ts
):
Payment Status API (app/api/payments/status/[orderId]/route.ts
):
User Courses API (app/api/user/courses/route.ts
):
Checkout Success Page (app/checkout/success/page.tsx
):
Create Checkout API (app/api/payments/create-checkout/route.ts
):
To complete the implementation, you'll need to:
This solution handles both payment providers (Stripe and Chargily) and ensures a consistent user experience regardless of which payment method is used.
Please make sure to add the following environment variables to your project:
/* eslint-disable @typescript-eslint/no-explicit-any */ import { db } from "@/utils/firebase" import { doc, getDoc, updateDoc, arrayUnion, deleteDoc, collection, query, where, getDocs, addDoc, } from "firebase/firestore" import { NextResponse } from "next/server" import Stripe from "stripe"
// Initialize Stripe const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: "2025-02-24.acacia", })
// This is your Stripe webhook secret for testing your endpoint locally. const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET
export async function POST(req: Request) { const payload = await req.text() const signature = req.headers.get("stripe-signature") as string
console.log("POST OF STRIPE");
let event
try {
event = stripe.webhooks.constructEvent(payload, signature, endpointSecret!)
} catch (err) {
console.error(Webhook signature verification failed: ${err instanceof Error ? err.message : String(err)}
)
return NextResponse.json({ error: "Webhook signature verification failed" }, { status: 400 })
}
// Handle the event if (event.type === "checkout.session.completed") { const session = event.data.object as Stripe.Checkout.Session
try {
await handleSuccessfulPayment(session)
return NextResponse.json({ received: true })
} catch (error) {
console.error("Error handling successful payment:", error)
return NextResponse.json({ error: "Error processing payment" }, { status: 500 })
}
}
return NextResponse.json({ received: true }) }
async function handleSuccessfulPayment(session: Stripe.Checkout.Session) { const orderId = session.metadata?.orderId const userId = session.metadata?.userId
console.log("Handle successful payment");
if (!orderId || !userId) { throw new Error("Missing order ID or user ID in session metadata") }
// 1. Get the order details
const orderDoc = await getDoc(doc(db, "orders", orderId))
if (!orderDoc.exists()) {
throw new Error(Order ${orderId} not found
)
}
const orderData = orderDoc.data() const courseIds = orderData.courses.map((course: any) => course.courseId)
// 2. Update the order status await updateDoc(doc(db, "orders", orderId), { status: "completed", paymentStatus: "completed", })
// 3. Add courses to user's purchased courses await updateDoc(doc(db, "users", userId), { purchasedCourses: arrayUnion(...courseIds), })
// 4. Create enrollment records for each course const now = new Date() for (const courseId of courseIds) { await createEnrollment(userId, courseId, now) }
// 5. Delete the user's cart const cartQuery = query(collection(db, "carts"), where("userId", "==", userId)) const cartSnapshot = await getDocs(cartQuery) console.log("Cart snapshot");
console.log(cartQuery , cartSnapshot);
if (!cartSnapshot.empty) { const cartDoc = cartSnapshot.docs[0] await deleteDoc(doc(db, "carts", cartDoc.id)) } }
async function createEnrollment(userId: string, courseId: string, enrolledAt: Date) { const enrollmentQuery = query( collection(db, "enrollments"), where("userId", "==", userId), where("courseId", "==", courseId), )
const enrollmentSnapshot = await getDocs(enrollmentQuery)
if (enrollmentSnapshot.empty) { // Create new enrollment if it doesn't exist await addDoc(collection(db, "enrollments"), { userId, courseId, status: "not_started", progress: 0, enrolledAt, lastAccessedAt: enrolledAt, }) } }
this dont show any logs its not working or something the page of stripe its shown and i fiel all the fields and after submit it take me to checkout page with success but this not work
It looks like your Stripe webhook isn't being triggered properly. This is a common issue with webhooks, especially during local development. Let's troubleshoot and fix this:
The issue you're experiencing is common with webhooks, especially during development. Here's what's happening and how I've fixed it:
I've implemented a comprehensive solution with three key components:
In create-checkout/route.ts
:
In webhooks/stripe/route.ts
:
Added manual-process/[orderId]/route.ts
:
Updated checkout/success/page.tsx
:
The order should be processed automatically:
For production, you'll need to:
https://your-domain.com/api/webhooks/stripe
This solution ensures your orders will be processed correctly in both development and production environments, even if there are issues with the webhook.
Error creating checkout session: [Error: Failed to make API request: Error: API request failed with status 422: Unprocessable Content] POST /api/payments/create-checkout 500 in 1558ms
Addons for programming languages JavaScript Requirements Node 8, 10 or higher. NPM Installation Via npm (Recomended)
Copy npm i chargily-epay-js
yarn add chargily-epay-js Quick start Add CHARGILY_APP_KEY and CHARGILY_APP_SECRET in .env file with the secret key and app key from [ePay Dashboard][api-keys]
#Usage The package needs to be configured with your account’s secret key, which is available in the [ePay Dashboard][api-keys]. Require it with the key’s value:
Copy const chargily = require('chargily-epay-js') const dotenv = require('dotenv') const {Invoice, Mode} = require("chargily-epay-js/lib/configuration");
dotenv.config()
const order = new Invoice() order.invoiceNumber = "100" // must be integer or string order.mode = Mode.EDAHABIA // or Mode.CIB order.backUrl = "https://www.exemple.org/" // must be a valid and active URL order.amount = 5000 // must be integer , and more or equal 75 order.webhookUrl = "https://www.exemple.org/webhook-validator" // this URL where receive the response order.client = "chawki mahdi" order.discount = 10 // by percentage between [0, 100] order.clientEmail = "client@example.com" // email of customer where he will receive the Bill order.appKey = process.env.CHARGILY_APP_KEY
// createPayment is promise function (async, await ), so you will need to use then to receive the checkoutURL
const checkoutUrl = chargily.createPayment(order).then( res => { return res.checkout_url // redirect to this url to proccess the checkout })
Configurations Available Configurations key description redirect url process url CHARGILY_APP_KEY must be string given by organization required required CHARGILY_APP_SECRET must be string given by organization required required back_url must be string and valid url required not required webhook_url must be string and valid url _ required required mode must be in CIB,EDAHABIA required not required invoice_number string or int required not required client_name string required not required clientEmail must be valid email This is where client receive payment receipt after confirmation required not required amount must be numeric and greather or equal than 75 required not required discount must be numeric and between 0 and 99 (discount in %) required not required description must be string_ required not required Testing Webhook signing You can use DefaultSignatureValidator.isValid() to validate incoming webhook.
Copy
const {DefaultSignatureValidator} = require("chargily-epay-js/lib/Webhook"); const express = require('express'); const app = express() const port = 3000 app.use(express.json());
app.post('/webhook-validator', (req, res)=>{ let signature = req.header('Signature')
let rs = DefaultSignatureValidator.isValid( signature, process.env.CHARGILY_APP_SECRET, req.body) // return boolean
res.send(rs) })
now fix it