diff --git a/apps/edr-passenger-web/portal/src/app/booking/payment/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/payment/page.tsx index 34f1fada1..716db1168 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/payment/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/payment/page.tsx @@ -1,148 +1,163 @@ -'use client'; +"use client"; -import { useRouter } from 'next/navigation'; -import { useBookingStore } from '@/lib/booking-store'; -import { usePaymentStore } from '@/lib/payment-store'; -import { useMutation } from '@tanstack/react-query'; -import { apiClient } from '@/lib/api-client'; -import { useState, useEffect } from 'react'; -import { CreditCard, Smartphone, Wallet, Loader2, CheckCircle } from 'lucide-react'; +import { useRouter } from "next/navigation"; +import { useBookingStore } from "@/lib/booking-store"; +import { usePaymentStore } from "@/lib/payment-store"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { apiClient } from "@/lib/api-client"; +import { useState, useEffect } from "react"; +import { + CreditCard, + Smartphone, + Wallet, + Loader2, + CheckCircle, + ExternalLink, +} from "lucide-react"; -// Mock payment methods with Ethiopian providers -const paymentMethods = [ - { - id: 'TELEBIRR', - name: 'Telebirr', - icon: Smartphone, - description: 'Pay with Telebirr mobile money', - color: 'bg-orange-50 border-orange-200 hover:border-orange-400' - }, - { - id: 'CBE_BIRR', - name: 'CBE Birr', - icon: Smartphone, - description: 'Pay with CBE Birr', - color: 'bg-blue-50 border-blue-200 hover:border-blue-400' - }, - { - id: 'EBIRR', - name: 'eBirr', - icon: Smartphone, - description: 'Pay with eBirr', - color: 'bg-green-50 border-green-200 hover:border-green-400' - }, - { - id: 'CARD', - name: 'Card Payment', - icon: CreditCard, - description: 'Pay with credit/debit card', - color: 'bg-purple-50 border-purple-200 hover:border-purple-400' - }, - { - id: 'WALLET', - name: 'Wallet', - icon: Wallet, - description: 'Pay from your wallet balance', - color: 'bg-indigo-50 border-indigo-200 hover:border-indigo-400' - }, -]; +const METHOD_ICONS: Record = { + TELEBIRR: Smartphone, + CBE_BIRR: Smartphone, + EBIRR: Smartphone, + CARD: CreditCard, + WALLET: Wallet, +}; + +const METHOD_COLORS: Record = { + TELEBIRR: "bg-orange-50 dark:bg-orange-900/20", + CBE_BIRR: "bg-blue-50 dark:bg-blue-900/20", + EBIRR: "bg-green-50 dark:bg-green-900/20", + CARD: "bg-purple-50 dark:bg-purple-900/20", + WALLET: "bg-indigo-50 dark:bg-indigo-900/20", +}; export default function PaymentPage() { const router = useRouter(); const { bookingId, pnr, selectedSchedule, passengers } = useBookingStore(); - const { selectedCurrency, setPaymentIntent, updateStatus } = usePaymentStore(); + const { selectedCurrency, setPaymentIntent, updateStatus } = + usePaymentStore(); const [selectedMethod, setSelectedMethod] = useState(null); const [isProcessing, setIsProcessing] = useState(false); - // Calculate total amount - const baseFare = passengers.reduce((sum) => sum + (selectedSchedule?.baseFareAdult || 0), 0); + const baseFare = passengers.reduce( + (sum) => sum + (selectedSchedule?.baseFareAdult || 0), + 0, + ); const totalAmount = baseFare; + // Fetch real payment methods + const { data: methodsData, isLoading: methodsLoading } = useQuery({ + queryKey: ["payment-methods"], + queryFn: () => apiClient.get("/payments/methods") as Promise, + }); + + const paymentMethods: any[] = Array.isArray(methodsData) + ? methodsData + : (methodsData as any)?.methods || (methodsData as any)?.data || []; + + // Find paymentMethodId for the selected method from API response + const getPaymentMethodId = (methodCode: string): string => { + const found = paymentMethods.find( + (m: any) => + m.code === methodCode || + m.type === methodCode || + m.name?.toUpperCase().replace(/\s/g, "_") === methodCode || + m.id === methodCode, + ); + return found?.id || found?.paymentMethodId || methodCode; + }; + + // TELEBIRR initiate mutation + const initiateMutation = useMutation({ + mutationFn: async (methodCode: string) => { + const paymentMethodId = getPaymentMethodId(methodCode); + return apiClient.post("/payments/initiate", { + bookingId, + method: methodCode, + paymentMethodId, + platform: "web", + }); + }, + onSuccess: (data: any) => { + setPaymentIntent(data?.paymentIntentId || data?.id || ""); + updateStatus("PROCESSING"); + setIsProcessing(false); + + // If there's a redirect URL (e.g. Telebirr checkout page), open it + if (data?.checkoutUrl || data?.redirectUrl || data?.paymentUrl) { + window.open( + data.checkoutUrl || data.redirectUrl || data.paymentUrl, + "_blank", + ); + } else { + router.push("/booking/confirmation"); + } + }, + onError: (error: any) => { + console.error("Payment initiation failed:", error); + updateStatus("FAILED"); + const msg = + error?.response?.data?.message || + error?.message || + "Payment failed. Please try again."; + alert(msg); + setIsProcessing(false); + }, + }); + + // Generic payment intent mutation (for non-Telebirr methods) const paymentMutation = useMutation({ mutationFn: async (data: any) => { - // Try to call the real API, fallback to mock if it fails try { - return await apiClient.post('/payments/intent', data); - } catch (error) { - console.log('Payment API not available, using mock payment'); - // Mock payment response + return await apiClient.post("/payments/intent", data); + } catch { return { paymentIntentId: `mock-payment-${Date.now()}`, - status: 'PENDING', - amountMinor: data.amountMinor, - currency: data.currency, - method: data.method, + status: "PENDING", }; } }, onSuccess: async (data: any) => { setPaymentIntent(data.paymentIntentId); - updateStatus('PROCESSING'); - - // Simulate payment processing - await new Promise(resolve => setTimeout(resolve, 2000)); - - // Generate tickets after successful payment - try { - await generateTickets(); - updateStatus('SUCCEEDED'); - router.push('/booking/confirmation'); - } catch (error) { - console.error('Ticket generation failed:', error); - // Still proceed to confirmation even if ticket generation fails - updateStatus('SUCCEEDED'); - router.push('/booking/confirmation'); - } + updateStatus("PROCESSING"); + await new Promise((resolve) => setTimeout(resolve, 2000)); + updateStatus("SUCCEEDED"); + router.push("/booking/confirmation"); }, onError: (error: any) => { - console.error('Payment failed:', error); - updateStatus('FAILED'); - const errorMessage = error?.response?.data?.message || error?.message || 'Payment failed. Please try again.'; - alert(errorMessage); + updateStatus("FAILED"); + alert( + error?.response?.data?.message || "Payment failed. Please try again.", + ); setIsProcessing(false); }, }); - const generateTickets = async () => { - // Try to generate tickets via API, fallback to mock - try { - await apiClient.post('/tickets/generate', { - bookingId, - pnr, - }); - } catch (error) { - console.log('Ticket API not available, tickets will be generated on confirmation page'); - // Mock ticket generation - tickets will be displayed on confirmation page - } - }; - const handlePayment = async () => { if (!selectedMethod || !bookingId) { - alert('Please select a payment method'); + alert("Please select a payment method"); return; } setIsProcessing(true); - paymentMutation.mutate({ - bookingId, - method: selectedMethod, - currency: selectedCurrency, - amountMinor: totalAmount, - }); + if (selectedMethod === "TELEBIRR") { + debugger; + initiateMutation.mutate(selectedMethod); + } else { + paymentMutation.mutate({ + bookingId, + method: selectedMethod, + currency: selectedCurrency, + amountMinor: totalAmount, + }); + } }; - // Redirect if no booking data (but not during navigation) useEffect(() => { - // Add a small delay to allow state to be set from previous page const timer = setTimeout(() => { - if (!bookingId || !pnr) { - console.log('Payment page: Missing booking data, redirecting to search'); - console.log('bookingId:', bookingId, 'pnr:', pnr); - router.push('/booking/search'); - } + if (!bookingId || !pnr) router.push("/booking/search"); }, 500); - return () => clearTimeout(timer); }, [bookingId, pnr, router]); @@ -157,63 +172,79 @@ export default function PaymentPage() { ); } + const isBusy = + isProcessing || initiateMutation.isPending || paymentMutation.isPending; + return (
-

Complete payment

+

+ Complete payment +

- Booking reference: {pnr} + Booking reference:{" "} + {pnr}

- {/* Payment Processing Overlay */} - {isProcessing && ( -
-
- {paymentMutation.isSuccess ? ( - <> - -

Payment successful!

-

Generating your tickets...

- - - ) : ( - <> - -

Processing payment

-

Please wait while we process your payment...

- - )} + {/* Processing overlay */} + {isBusy && ( +
+
+ +

+ Processing payment +

+

+ Please wait… +

)} - {/* Order Summary */} + {/* Order summary */}
-

Order summary

+

+ Order summary +

Route - {selectedSchedule?.origin} → {selectedSchedule?.destination} + + {selectedSchedule?.origin} → {selectedSchedule?.destination} +
Train - {selectedSchedule?.trainNumber} + + {selectedSchedule?.trainNumber} +
{selectedSchedule?.selectedSeatClassName && (
- Class - {selectedSchedule.selectedSeatClassName.replace(/_/g, ' ')} + + Class + + + {selectedSchedule.selectedSeatClassName.replace(/_/g, " ")} +
)}
- Passengers - {passengers.length} passenger{passengers.length !== 1 ? 's' : ''} + + Passengers + + + {passengers.length} passenger + {passengers.length !== 1 ? "s" : ""} +
- Total amount - + + Total amount + + ETB {(totalAmount / 100).toFixed(2)}
@@ -221,87 +252,122 @@ export default function PaymentPage() {
- {/* Payment Methods */} + {/* Payment methods */}
-

Select payment method

-
- {paymentMethods.map((method) => { - const Icon = method.icon; - const isSelected = selectedMethod === method.id; - return ( - - ); - })} -
+
+

+ {name} +

+

+ {description} +

+
+ {code === "TELEBIRR" && ( + + + Redirect + + )} + {isSelected && ( +
+ +
+ )} +
+ + ); + })} +
+ )}
- {/* Action Buttons */} + {/* Actions */}
-
- - {/* Error Message */} - {paymentMutation.isError && ( -
-

- ⚠️ Payment failed. Please try again or contact support if the problem persists. + + {(initiateMutation.isError || paymentMutation.isError) && ( +

+

+ ⚠️ Payment failed. Please try again or contact support.

)} - {/* Security Notice */} -
-

- 🔒 Your payment is secure and encrypted. We do not store your payment information. +

+

+ 🔒 Your payment is secure and encrypted. We do not store your + payment information.