mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 20:40:55 +00:00
Merge remote-tracking branch 'origin/staging' into freight/fix/pay
# Conflicts: # apps/edr-freight-api/src/modules/billing/billing.service.ts # apps/edr-freight-web/portal/src/pages/billing/InvoiceDetailPage.tsx # apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/PaymentMethodModal.tsx # pnpm-lock.yaml
This commit is contained in:
@@ -200,6 +200,7 @@ export default function PaymentMethodsPage() {
|
||||
const paymentTypes = [
|
||||
{ value: 'TELEBIRR', label: 'Telebirr' },
|
||||
{ value: 'CBE_BIRR', label: 'CBE Birr' },
|
||||
{ value: 'CBE_BILL', label: 'CBE Bill Payment' },
|
||||
{ value: 'EBIRR', label: 'eBirr' },
|
||||
{ value: 'WAAFI', label: 'Waafi' },
|
||||
{ value: 'DMONEY', label: 'dMoney' },
|
||||
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
ChevronLeft,
|
||||
KeyRound,
|
||||
Landmark,
|
||||
Copy,
|
||||
Check,
|
||||
} from "lucide-react";
|
||||
|
||||
const getIconForMethod = (methodId: string) => {
|
||||
@@ -45,6 +47,13 @@ export default function PaymentPage() {
|
||||
const [otpCode, setOtpCode] = useState("");
|
||||
const [otpMessage, setOtpMessage] = useState<string | null>(null);
|
||||
const [otpError, setOtpError] = useState<string | null>(null);
|
||||
// CBE bill payment: the bill reference the customer pays at any CBE channel.
|
||||
const [billAction, setBillAction] = useState<{
|
||||
billReference: string;
|
||||
instructions?: string;
|
||||
expiresAt?: string;
|
||||
} | null>(null);
|
||||
const [billCopied, setBillCopied] = useState(false);
|
||||
|
||||
const isRoundTrip = searchCriteria?.tripType === 'ROUND_TRIP';
|
||||
|
||||
@@ -144,6 +153,16 @@ export default function PaymentPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
// CBE bill: no redirect — show the bill reference and wait for the customer to pay
|
||||
// at a CBE channel. Confirmation only ever comes from polling the intent status.
|
||||
if (data?.clientAction?.type === 'SHOW_BILL_REFERENCE') {
|
||||
setPaymentIntent(data.intentId);
|
||||
updateStatus("REQUIRES_ACTION");
|
||||
setBillAction(data.clientAction);
|
||||
setIsProcessing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((selectedMethod === 'TELEBIRR' || selectedMethod === 'WAAFI' || selectedMethod === 'DMONEY') && data?.clientAction?.type === 'REDIRECT') {
|
||||
setPaymentIntent(data.intentId);
|
||||
updateStatus("REQUIRES_ACTION");
|
||||
@@ -190,6 +209,34 @@ export default function PaymentPage() {
|
||||
|
||||
|
||||
|
||||
// While the CBE bill dialog is open, poll the intent; the booking confirms server-side
|
||||
// once CBE settles the bill. Never claim success on any client-side signal.
|
||||
const { data: billIntentStatus } = useQuery<any>({
|
||||
queryKey: ["cbe-bill-intent-status", bookingId],
|
||||
queryFn: () => apiClient.get(`/payments/intents/${bookingId}`),
|
||||
enabled: !!billAction && !!bookingId,
|
||||
refetchInterval: 5_000,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (billAction && billIntentStatus?.status === "SUCCEEDED") {
|
||||
updateStatus("SUCCEEDED");
|
||||
router.push("/booking/confirmation");
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [billAction, billIntentStatus?.status]);
|
||||
|
||||
const copyBillReference = async () => {
|
||||
if (!billAction) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(billAction.billReference);
|
||||
setBillCopied(true);
|
||||
setTimeout(() => setBillCopied(false), 2000);
|
||||
} catch {
|
||||
/* clipboard unavailable — the number is still shown on screen */
|
||||
}
|
||||
};
|
||||
|
||||
// Fire the actual initiate. `mobile` is only used for CAC (OTP debit).
|
||||
const startPayment = (mobile?: string) => {
|
||||
if (!selectedMethod || !bookingId || !selectedPaymentMethod) return;
|
||||
@@ -569,6 +616,62 @@ export default function PaymentPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* CBE bill payment — show the bill reference; confirmation comes from polling */}
|
||||
{billAction && (
|
||||
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 px-4">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl p-6 max-w-md w-full shadow-2xl">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Landmark className="w-5 h-5 text-primary" />
|
||||
<h3 className="text-lg font-bold text-gray-900 dark:text-gray-100">Pay at CBE</h3>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">
|
||||
{billAction.instructions ??
|
||||
"Pay this bill at any CBE branch, the CBE Birr app, mobile banking or USSD."}
|
||||
</p>
|
||||
<div className="flex items-center justify-between gap-2 bg-gray-50 dark:bg-gray-900/40 border border-gray-200 dark:border-gray-700 rounded-lg px-4 py-3">
|
||||
<span className="font-mono text-2xl font-bold tracking-widest text-gray-900 dark:text-gray-100 select-all">
|
||||
{billAction.billReference}
|
||||
</span>
|
||||
<button
|
||||
onClick={copyBillReference}
|
||||
className="btn-secondary px-3 py-2 flex items-center gap-1 text-sm"
|
||||
title="Copy bill number"
|
||||
>
|
||||
{billCopied ? <Check className="w-4 h-4 text-green-600" /> : <Copy className="w-4 h-4" />}
|
||||
{billCopied ? "Copied" : "Copy"}
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-300 mt-3 space-y-1">
|
||||
<p>
|
||||
Amount:{" "}
|
||||
<span className="font-semibold">
|
||||
ETB {(totalAmountDisplay ?? 0).toFixed(2)}
|
||||
</span>
|
||||
</p>
|
||||
{billAction.expiresAt && (
|
||||
<p>
|
||||
Pay before:{" "}
|
||||
<span className="font-semibold">
|
||||
{format(new Date(billAction.expiresAt), "MMM d, yyyy HH:mm")}
|
||||
</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-4 text-xs text-gray-500 dark:text-gray-400">
|
||||
<Loader2 className="w-4 h-4 animate-spin flex-shrink-0" />
|
||||
Waiting for payment confirmation — this page updates automatically once CBE
|
||||
confirms your payment.
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setBillAction(null)}
|
||||
className="btn-secondary w-full py-2.5 mt-4"
|
||||
>
|
||||
Close (pay later)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Two-column grid */}
|
||||
<div className="lg:grid lg:grid-cols-3 lg:gap-6 lg:items-start">
|
||||
|
||||
|
||||
Reference in New Issue
Block a user