Merge branch 'staging' into dev

This commit is contained in:
Nathnael Wondisha
2026-08-01 14:06:37 +03:00
committed by GitHub
56 changed files with 2348 additions and 316 deletions

View File

@@ -68,6 +68,7 @@ const METHOD_OPTIONS: { value: PaymentMethod; label: string }[] = [
{ value: "card", label: "Card" },
{ value: "dmoney", label: "D-Money" },
{ value: "cac-bank", label: "CAC Bank" },
{ value: "cbe-bill", label: "CBE Bill" },
];
const STATUS_COLORS: Record<string, string> = {

View File

@@ -19,7 +19,8 @@ export type PaymentMethod =
| "waafi"
| "card"
| "dmoney"
| "cac-bank";
| "cac-bank"
| "cbe-bill";
export interface PaymentRow {
id: string;

View File

@@ -9,6 +9,7 @@ import {
Divider,
Group,
Loader,
Modal,
Paper,
SimpleGrid,
Stack,
@@ -69,6 +70,12 @@ export default function InvoiceDetailPage() {
} = useQuery(api.invoices.get.queryOptions({ input: { id } }));
const [payModalOpen, setPayModalOpen] = useState(false);
// CBE bill payment: the bill reference to pay at any CBE channel (no redirect).
const [billAction, setBillAction] = useState<{
billReference?: string;
instructions?: string;
expiresAt?: string;
} | null>(null);
// Ownership-checked: POST /billing/my-invoices/:id/pay only ever charges
// one of the signed-in customer's own invoices (unlike the admin-facing
@@ -377,6 +384,62 @@ export default function InvoiceDetailPage() {
pay.pay(id, method, payerAccount)
}
/>
{/* CBE bill payment — show the bill number; settlement arrives via CBE, not the browser */}
<Modal
opened={!!billAction}
onClose={() => setBillAction(null)}
centered
radius={18}
size={440}
title={<Text fw={800}>Pay at CBE</Text>}
>
<Stack gap="sm">
<Text fz="sm" c={MUTED}>
{billAction?.instructions ??
"Pay this bill at any CBE branch, the CBE Birr app, mobile banking or USSD."}
</Text>
<Group
justify="space-between"
px={16}
py={13}
style={{ borderRadius: 12, border: `1px solid ${BORDER}` }}
>
<Text ff="monospace" fz={24} fw={800} c={INK} style={{ letterSpacing: 3 }}>
{billAction?.billReference}
</Text>
<Button
variant="light"
size="xs"
onClick={() => {
if (billAction?.billReference) {
navigator.clipboard?.writeText(billAction.billReference);
toast.success("Bill number copied");
}
}}
>
Copy
</Button>
</Group>
<Text fz="sm" c={MUTED}>
Amount due:{" "}
<Text span fw={700} c={INK}>
{formatCurrency(amountDue, invoice.currency)}
</Text>
</Text>
{billAction?.expiresAt && (
<Text fz="sm" c={MUTED}>
Pay before:{" "}
<Text span fw={700} c={INK}>
{fmtDate(billAction.expiresAt)}
</Text>
</Text>
)}
<Text fz="xs" c={MUTED}>
The invoice updates automatically once CBE confirms your payment.
</Text>
</Stack>
</Modal>
</Stack>
</Box>
);

View File

@@ -26,7 +26,7 @@ interface ProviderOption {
accent: string;
}
// Only Telebirr, Waafi and CAC Bank are enabled for now.
// Only Telebirr, Waafi and CBE bill payment are enabled for now.
const PROVIDERS: ProviderOption[] = [
{
method: "TELEBIRR",
@@ -51,6 +51,14 @@ const PROVIDERS: ProviderOption[] = [
currencies: ["USD"],
accent: "#8A5A17",
},
{
method: "CBE_BILL",
label: "CBE bill payment",
description: "Pay at any CBE branch, app or USSD · ETB",
logo: "/assets/edr-logo.png",
currencies: ["ETB"],
accent: "#5B2D8C",
},
];
/** Providers that debit against an SMS OTP instead of redirecting to a page. */

View File

@@ -12,7 +12,8 @@ export type PaymentMethod =
| "WAAFI"
| "CARD"
| "DMONEY"
| "CAC_BANK";
| "CAC_BANK"
| "CBE_BILL";
export type PaymentPlatform = "web" | "mobile";
@@ -26,13 +27,17 @@ export interface InitiatePaymentPayload {
}
export interface ClientAction {
type: "REDIRECT" | "LAUNCH_APP" | "COLLECT_OTP";
type: "REDIRECT" | "LAUNCH_APP" | "COLLECT_OTP" | "SHOW_BILL_REFERENCE";
url?: string;
appId?: string;
receiveCode?: string;
shortCode?: string;
providerOrderId?: string;
message?: string;
/** SHOW_BILL_REFERENCE (CBE bill payment) */
billReference?: string;
instructions?: string;
expiresAt?: string;
}
export interface InitiateResponse {