mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-06 06:33:39 +00:00
feat(freight-portal): support DJF as a billing and payment currency
lib/currency.ts re-exports the shared @edr/ui-common formatter (was a local implementation always forcing 2 decimals, wrong for DJF). Currency pickers (new-booking-form, new-contract-form, new-shipment Currency selector and schemas) offer DJF wherever USD is offered. The bigger piece: offline-payment.ts's isUsdCurrency/isUsdOfflineBooking assumed exactly two payment rails (ETB online, USD offline) and picked one. DJF supports BOTH, so it's replaced with independent canPayOnline/canPayOffline predicates, updated across the 5 call sites that gated the Pay button vs. the bank-transfer badge. PaymentMethodModal's WAAFI/CAC Bank entries (Djibouti gateways mislabeled USD-only) now list DJF, and a currency that matches no provider returns no providers instead of silently offering all of them (was returning every provider, including the ETB-only one, on any unmatched currency). Claude-Session: https://claude.ai/code/session_01CZy77vCWhka3pnmVF9NDkL
This commit is contained in:
@@ -1,23 +1,8 @@
|
||||
/** Currency code carried on invoices / dashboard figures (ETB, USD, DJF, …). */
|
||||
export type Currency = string;
|
||||
|
||||
const SYMBOLS: Record<string, string> = {
|
||||
USD: "$",
|
||||
ETB: "Br",
|
||||
DJF: "DJF",
|
||||
};
|
||||
|
||||
/**
|
||||
* Format a money amount with its currency symbol, e.g. `Br 12,500.00`.
|
||||
* Unknown currency codes fall back to printing the raw code.
|
||||
* Currency code carried on invoices / dashboard figures (ETB, USD, DJF, …).
|
||||
* Re-exports the shared `@edr/ui-common` currency module so every currency
|
||||
* gets the same symbol, decimals rule and formatting across both freight web
|
||||
* apps — see that module for the source of truth.
|
||||
*/
|
||||
export function formatCurrency(
|
||||
amount: number,
|
||||
currency: Currency = "ETB",
|
||||
): string {
|
||||
const symbol = SYMBOLS[currency] ?? currency;
|
||||
return `${symbol} ${Number(amount ?? 0).toLocaleString(undefined, {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})}`;
|
||||
}
|
||||
export type { SupportedCurrency as Currency } from "@edr/ui-common";
|
||||
export { formatCurrency, currencySymbol, currencyDecimals, CURRENCY_CODES } from "@edr/ui-common";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Freight } from "@edr/types";
|
||||
|
||||
import { isUsdOfflineBooking } from "@/pages/bookings/payments/offline-payment";
|
||||
import { bookingCanPayOnline } from "@/pages/bookings/payments/offline-payment";
|
||||
|
||||
/** A pending customer action surfaced on the home "needs attention" card. */
|
||||
export interface ActionItem {
|
||||
@@ -64,7 +64,10 @@ export function deriveActionItems(
|
||||
? b.status === "FULLY_EXECUTED"
|
||||
: b.status === "SELECTED_FOR_BATCH");
|
||||
if (canPay) {
|
||||
const offlinePay = isUsdOfflineBooking(b);
|
||||
// Online-only description unless the booking's currency has no online
|
||||
// rail at all (USD) — DJF supports both, so it reads as a normal
|
||||
// "payment due" like ETB rather than bank-transfer-only.
|
||||
const offlinePay = !bookingCanPayOnline(b);
|
||||
items.push({
|
||||
id: `pay-${b.id}`,
|
||||
kind: "pay",
|
||||
|
||||
@@ -32,7 +32,7 @@ import { invoicesService } from "@/services/invoices.service";
|
||||
import { useInvoicePayment } from "@/hooks/useInvoicePayment";
|
||||
import { warehouseInvoicesService } from "@/services/warehouse-invoices.service";
|
||||
import { PaymentMethodModal } from "@/pages/bookings/BookingDetailPage/components/PaymentMethodModal";
|
||||
import { isUsdCurrency } from "@/pages/bookings/payments/offline-payment";
|
||||
import { canPayOffline, canPayOnline } from "@/pages/bookings/payments/offline-payment";
|
||||
import { saveBlob } from "@/utils/download";
|
||||
import { formatCurrency } from "@/lib/currency";
|
||||
import { BORDER, INK, MUTED } from "../contracts/contract-ui";
|
||||
@@ -232,7 +232,7 @@ export default function InvoiceDetailPage() {
|
||||
Receipt
|
||||
</Button>
|
||||
)}
|
||||
{payable && !isUsdCurrency(invoice.currency) && (
|
||||
{payable && canPayOnline(invoice.currency) && (
|
||||
<Button
|
||||
color="edr-green"
|
||||
radius="md"
|
||||
@@ -247,7 +247,7 @@ export default function InvoiceDetailPage() {
|
||||
Pay {formatCurrency(amountDue, invoice.currency)}
|
||||
</Button>
|
||||
)}
|
||||
{payable && isUsdCurrency(invoice.currency) && (
|
||||
{payable && canPayOffline(invoice.currency) && (
|
||||
<Badge
|
||||
size="lg"
|
||||
radius="md"
|
||||
|
||||
@@ -32,7 +32,7 @@ import { Freight } from "@edr/types";
|
||||
|
||||
import { api } from "@/services/api";
|
||||
import { formatCurrency } from "@/lib/currency";
|
||||
import { isUsdCurrency } from "@/pages/bookings/payments/offline-payment";
|
||||
import { canPayOnline } from "@/pages/bookings/payments/offline-payment";
|
||||
import {
|
||||
BORDER,
|
||||
GREEN,
|
||||
@@ -277,10 +277,11 @@ export default function InvoicesList() {
|
||||
{!isLoading &&
|
||||
!isError &&
|
||||
pageRows.map((inv) => {
|
||||
// USD invoices are paid by bank transfer — the detail page
|
||||
// shows the instructions, so the row action reads "View".
|
||||
// A currency with no online rail (USD) is paid by bank
|
||||
// transfer — the detail page shows the instructions, so the
|
||||
// row action reads "View".
|
||||
const payable =
|
||||
isPayable(inv.status) && !isUsdCurrency(inv.currency);
|
||||
isPayable(inv.status) && canPayOnline(inv.currency);
|
||||
return (
|
||||
<Table.Tr
|
||||
key={inv.id}
|
||||
|
||||
@@ -17,7 +17,7 @@ import type { Freight } from "@edr/types";
|
||||
import { invoicesService, type PortalInvoice } from "@/services/invoices.service";
|
||||
import { InvoiceStatusBadge, titleCase } from "@/pages/billing/invoice-ui";
|
||||
import { paymentStatusLabel } from "@/pages/bookings/booking-display";
|
||||
import { isUsdOfflineBooking } from "@/pages/bookings/payments/offline-payment";
|
||||
import { bookingCanPayOffline, bookingCanPayOnline } from "@/pages/bookings/payments/offline-payment";
|
||||
import { PayerAccountNote } from "@/pages/bookings/payments/PayerAccountNote";
|
||||
import { payWindowState } from "@/pages/bookings/payments/payment-drain";
|
||||
import { PaymentProcessingNotice } from "@/pages/bookings/payments/PaymentProcessingNotice";
|
||||
@@ -200,7 +200,8 @@ export function BookingPaymentPanel({
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
const paid = booking.paymentStatus === "PAID";
|
||||
const offlineUsd = isUsdOfflineBooking(booking);
|
||||
const payOnline = bookingCanPayOnline(booking);
|
||||
const payOffline = bookingCanPayOffline(booking);
|
||||
const isAdjusted =
|
||||
booking.adjustedTotalAmount !== null &&
|
||||
booking.adjustedTotalAmount !== undefined;
|
||||
@@ -293,7 +294,7 @@ export function BookingPaymentPanel({
|
||||
<PaymentProcessingNotice drainEndsAt={payWindow.drainEndsAt} />
|
||||
)}
|
||||
|
||||
{!paid && !draining && offlineUsd && (
|
||||
{!paid && !draining && payOffline && (
|
||||
<Box
|
||||
mt={14}
|
||||
p={14}
|
||||
@@ -307,10 +308,9 @@ export function BookingPaymentPanel({
|
||||
Pay by bank transfer
|
||||
</Text>
|
||||
<Text mt={4} fz="12.5px" c="#7A5A1E" lh={1.55}>
|
||||
Online payment isn't available for USD bookings. Transfer the
|
||||
total amount to EDR's bank account before the payment deadline,
|
||||
then send the payment slip to the EDR Finance department — they
|
||||
will confirm your payment.
|
||||
{payOnline
|
||||
? "You can also transfer the total amount to EDR\u2019s bank account before the payment deadline, then send the payment slip to the EDR Finance department \u2014 they will confirm your payment."
|
||||
: "Online payment isn\u2019t available for this booking\u2019s currency. Transfer the total amount to EDR\u2019s bank account before the payment deadline, then send the payment slip to the EDR Finance department \u2014 they will confirm your payment."}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
@@ -319,7 +319,7 @@ export function BookingPaymentPanel({
|
||||
<Box mt={16}>
|
||||
<Countdown
|
||||
deadline={booking.paymentDeadline}
|
||||
onPay={offlineUsd ? undefined : onPay}
|
||||
onPay={payOnline ? onPay : undefined}
|
||||
paying={paying}
|
||||
/>
|
||||
{!paid && <PayerAccountNote />}
|
||||
|
||||
@@ -44,16 +44,16 @@ const PROVIDERS: ProviderOption[] = [
|
||||
{
|
||||
method: "WAAFI",
|
||||
label: "Waafi",
|
||||
description: "Djibouti mobile money · USD",
|
||||
description: "Djibouti mobile money · USD or DJF",
|
||||
logo: "/assets/waafi.jpeg",
|
||||
currencies: ["USD"],
|
||||
currencies: ["USD", "DJF"],
|
||||
accent: "#2E5B96",
|
||||
},
|
||||
{
|
||||
method: "CAC_BANK",
|
||||
label: "CAC Bank",
|
||||
description: "Djibouti bank debit · confirmed by SMS OTP",
|
||||
currencies: ["USD"],
|
||||
currencies: ["USD", "DJF"],
|
||||
accent: "#8A5A17",
|
||||
},
|
||||
{
|
||||
@@ -74,14 +74,16 @@ const OTP_LENGTH = 4;
|
||||
const isBillMethod = (method: PaymentMethod) => method === "CBE_BILL";
|
||||
|
||||
/**
|
||||
* Pick the provider that settles in the booking's currency. USD → Waafi/CAC,
|
||||
* ETB → CBE bill. Falls back to the full list when unknown.
|
||||
* Pick the provider(s) that settle in the booking's currency: ETB → CBE
|
||||
* bill, USD/DJF → Waafi/CAC. Falls back to the full list when the currency
|
||||
* is unknown, but a *known* currency with no matching provider (ETB and USD
|
||||
* never share one) returns nothing rather than every provider — offering
|
||||
* CBE bill for a DJF invoice, say, would settle it in the wrong currency.
|
||||
*/
|
||||
function providersForCurrency(currency?: string | null): ProviderOption[] {
|
||||
const cur = currency?.trim().toUpperCase();
|
||||
if (!cur) return PROVIDERS;
|
||||
const matched = PROVIDERS.filter((p) => p.currencies.includes(cur));
|
||||
return matched.length > 0 ? matched : PROVIDERS;
|
||||
return PROVIDERS.filter((p) => p.currencies.includes(cur));
|
||||
}
|
||||
|
||||
function ProviderRow({
|
||||
@@ -213,14 +215,14 @@ export function PaymentMethodModal({
|
||||
),
|
||||
[currency, otp, bill],
|
||||
);
|
||||
const [method, setMethod] = useState<PaymentMethod>(providers[0].method);
|
||||
const [method, setMethod] = useState<PaymentMethod>(providers[0]?.method ?? PROVIDERS[0].method);
|
||||
const [mobile, setMobile] = useState("");
|
||||
const [code, setCode] = useState("");
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
// Keep the selection valid when the currency (and therefore provider list) changes.
|
||||
useEffect(() => {
|
||||
if (!providers.some((p) => p.method === method)) {
|
||||
if (providers.length > 0 && !providers.some((p) => p.method === method)) {
|
||||
setMethod(providers[0].method);
|
||||
}
|
||||
}, [providers, method]);
|
||||
@@ -462,14 +464,21 @@ export function PaymentMethodModal({
|
||||
Payment method
|
||||
</Text>
|
||||
<Stack gap={10}>
|
||||
{providers.map((option) => (
|
||||
<ProviderRow
|
||||
key={option.method}
|
||||
option={option}
|
||||
selected={method === option.method}
|
||||
onSelect={() => setMethod(option.method)}
|
||||
/>
|
||||
))}
|
||||
{providers.length === 0 ? (
|
||||
<Text fz="12.5px" c="dimmed">
|
||||
No online payment method is available for this currency yet — use bank
|
||||
transfer instead.
|
||||
</Text>
|
||||
) : (
|
||||
providers.map((option) => (
|
||||
<ProviderRow
|
||||
key={option.method}
|
||||
option={option}
|
||||
selected={method === option.method}
|
||||
onSelect={() => setMethod(option.method)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{needsMobile && (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Box, Group, Text } from "@mantine/core";
|
||||
import { Banknote, Check, DollarSign } from "lucide-react";
|
||||
import { Banknote, Check, Coins, DollarSign } from "lucide-react";
|
||||
import { Controller, type Control } from "react-hook-form";
|
||||
import {
|
||||
PAYMENT_CURRENCY_OPTIONS,
|
||||
@@ -15,6 +15,7 @@ const CURRENCY_ICONS: Record<
|
||||
> = {
|
||||
USD: { icon: DollarSign, color: "#4F46E5" },
|
||||
ETB: { icon: Banknote, color: "#0A6F4D" },
|
||||
DJF: { icon: Coins, color: "#B45309" },
|
||||
};
|
||||
|
||||
export function PaymentCurrencyField({
|
||||
@@ -28,9 +29,10 @@ export function PaymentCurrencyField({
|
||||
*/
|
||||
allowUsd?: boolean;
|
||||
}) {
|
||||
// DJF is offered wherever USD is — both are import-shipment-only currencies.
|
||||
const options = allowUsd
|
||||
? PAYMENT_CURRENCY_OPTIONS
|
||||
: PAYMENT_CURRENCY_OPTIONS.filter((o) => o.value !== "USD");
|
||||
: PAYMENT_CURRENCY_OPTIONS.filter((o) => o.value === "ETB");
|
||||
return (
|
||||
<Box mt={24}>
|
||||
<StepLabel>Payment currency</StepLabel>
|
||||
|
||||
@@ -73,7 +73,7 @@ export const BOOKING_DOCS_SETTING: Freight.IFileUploadSetting = {
|
||||
|
||||
export type BookingDocuments = Record<string, File | File[] | null>;
|
||||
|
||||
export const PAYMENT_CURRENCIES = ["USD", "ETB"] as const;
|
||||
export const PAYMENT_CURRENCIES = ["USD", "ETB", "DJF"] as const;
|
||||
export type PaymentCurrency = (typeof PAYMENT_CURRENCIES)[number];
|
||||
|
||||
export const PAYMENT_CURRENCY_OPTIONS: Array<{
|
||||
@@ -92,6 +92,12 @@ export const PAYMENT_CURRENCY_OPTIONS: Array<{
|
||||
label: "USD",
|
||||
description: "US Dollar — paid by bank transfer, slip sent to Finance.",
|
||||
},
|
||||
// Import shipments only, same as USD.
|
||||
{
|
||||
value: "DJF",
|
||||
label: "DJF",
|
||||
description: "Djibouti Franc — paid online, or by bank transfer.",
|
||||
},
|
||||
];
|
||||
|
||||
export const BOOKING_TYPES = ["one_time", "general_contract"] as const;
|
||||
|
||||
@@ -1,16 +1,33 @@
|
||||
import type { Freight } from "@edr/types";
|
||||
|
||||
/**
|
||||
* USD bookings are never paid online: the customer pays by bank transfer and
|
||||
* the Finance department confirms the payment from the slip. Phase 1 is
|
||||
* portal-only — Finance's confirm flow lands in the backoffice later.
|
||||
* Which payment rails a currency supports. ETB settles online only (the
|
||||
* payment gateway); USD is bank-transfer-only (never through the online
|
||||
* gateway) — the customer pays by bank transfer and the Finance department
|
||||
* confirms the payment from the slip. DJF supports BOTH: it settles through
|
||||
* a Djibouti gateway (WAAFI / CAC Bank) as well as by bank transfer.
|
||||
*
|
||||
* These two predicates are intentionally independent, not opposites of one
|
||||
* currency check — DJF is neither purely online nor purely offline.
|
||||
*/
|
||||
export function isUsdCurrency(currency?: string | null): boolean {
|
||||
return currency?.toUpperCase() === "USD";
|
||||
export function canPayOnline(currency?: string | null): boolean {
|
||||
const c = currency?.toUpperCase();
|
||||
return c === "ETB" || c === "DJF";
|
||||
}
|
||||
|
||||
export function isUsdOfflineBooking(booking: Freight.IBooking): boolean {
|
||||
return isUsdCurrency(
|
||||
booking.pricingBreakdown?.currency ?? booking.paymentCurrency,
|
||||
);
|
||||
export function canPayOffline(currency?: string | null): boolean {
|
||||
const c = currency?.toUpperCase();
|
||||
return c === "USD" || c === "DJF";
|
||||
}
|
||||
|
||||
function bookingCurrency(booking: Freight.IBooking): string | null | undefined {
|
||||
return booking.pricingBreakdown?.currency ?? booking.paymentCurrency;
|
||||
}
|
||||
|
||||
export function bookingCanPayOnline(booking: Freight.IBooking): boolean {
|
||||
return canPayOnline(bookingCurrency(booking));
|
||||
}
|
||||
|
||||
export function bookingCanPayOffline(booking: Freight.IBooking): boolean {
|
||||
return canPayOffline(bookingCurrency(booking));
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { isPayable } from "@/pages/billing/invoice-ui";
|
||||
import { bookingsService } from "@/services/bookings.service";
|
||||
import { invoicesService } from "@/services/invoices.service";
|
||||
|
||||
import { isUsdOfflineBooking } from "./offline-payment";
|
||||
import { bookingCanPayOnline } from "./offline-payment";
|
||||
import { WAGON_CANCEL_FEE_INVOICE_TYPE } from "./useBookingPayment";
|
||||
|
||||
export type PayableAction =
|
||||
@@ -77,7 +77,11 @@ export function useBookingPayables(booking: Freight.IBooking) {
|
||||
|
||||
const items = useMemo(() => {
|
||||
const out: PayableItem[] = [];
|
||||
const offline = isUsdOfflineBooking(booking);
|
||||
// The strip's single action per item picks online when it's available at
|
||||
// all (DJF supports both rails; the freight-payment card itself offers
|
||||
// both) and falls back to the bank-transfer instructions only when
|
||||
// online isn't an option (USD).
|
||||
const offline = !bookingCanPayOnline(booking);
|
||||
|
||||
for (const inv of invoicesQ.data ?? []) {
|
||||
const balance = Number(inv.balanceAmount ?? 0);
|
||||
|
||||
@@ -310,7 +310,9 @@ function mapBookingToShipmentValues(
|
||||
// Resubmit keeps the currency the customer already chose on this booking;
|
||||
// a missing value falls back to empty so the choice is made deliberately.
|
||||
paymentCurrency:
|
||||
booking.paymentCurrency === "USD" || booking.paymentCurrency === "ETB"
|
||||
booking.paymentCurrency === "USD" ||
|
||||
booking.paymentCurrency === "ETB" ||
|
||||
booking.paymentCurrency === "DJF"
|
||||
? booking.paymentCurrency
|
||||
: "",
|
||||
withReturn: booking.equipmentReturn === "WITH_RETURN",
|
||||
@@ -1464,7 +1466,7 @@ function ScheduleStep({
|
||||
<StepLabel>Billing currency *</StepLabel>
|
||||
<Text fz={12.5} c="dimmed" mt={4} mb={10}>
|
||||
{isImport
|
||||
? "Import shipments may be invoiced in ETB or USD. USD is paid by bank transfer, not online."
|
||||
? "Import shipments may be invoiced in ETB, USD or DJF. USD is paid by bank transfer, not online."
|
||||
: "Shipments are invoiced in ETB."}
|
||||
</Text>
|
||||
<CurrencySelector
|
||||
@@ -1472,6 +1474,7 @@ function ScheduleStep({
|
||||
onChange={(v) => field.onChange(v)}
|
||||
error={fieldState.error?.message}
|
||||
allowUsd={isImport}
|
||||
allowDjf={isImport}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -38,7 +38,7 @@ export default function NewShipmentRequestPage() {
|
||||
// to be invoiced in has to be stated here — the contract itself quotes USD.
|
||||
// Starts empty so the billing-currency choice is deliberate — required at
|
||||
// submit. Intercity/export are forced to ETB (server-enforced too).
|
||||
const [paymentCurrency, setPaymentCurrency] = useState<"USD" | "ETB" | "">("");
|
||||
const [paymentCurrency, setPaymentCurrency] = useState<"USD" | "ETB" | "DJF" | "">("");
|
||||
const [currencyError, setCurrencyError] = useState<string | undefined>();
|
||||
const [notes, setNotes] = useState("");
|
||||
|
||||
@@ -125,7 +125,7 @@ export default function NewShipmentRequestPage() {
|
||||
contractRouteId: route?.id,
|
||||
scheduledDate: hasCustoms ? undefined : scheduledDate || undefined,
|
||||
paymentCurrency:
|
||||
isIntercity || isExport ? "ETB" : (paymentCurrency as "USD" | "ETB"),
|
||||
isIntercity || isExport ? "ETB" : (paymentCurrency as "USD" | "ETB" | "DJF"),
|
||||
notes: notes.trim() || undefined,
|
||||
};
|
||||
|
||||
@@ -267,6 +267,7 @@ export default function NewShipmentRequestPage() {
|
||||
}}
|
||||
disabled={isIntercity || isExport}
|
||||
allowUsd={!isIntercity && !isExport}
|
||||
allowDjf={!isIntercity && !isExport}
|
||||
error={currencyError}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -75,7 +75,7 @@ export const CONTRACT_KIND_OPTIONS: Array<{
|
||||
|
||||
export type ContractDocuments = Record<string, File | File[] | null>;
|
||||
|
||||
export const PAYMENT_CURRENCIES = ["USD", "ETB"] as const;
|
||||
export const PAYMENT_CURRENCIES = ["USD", "ETB", "DJF"] as const;
|
||||
export type PaymentCurrency = (typeof PAYMENT_CURRENCIES)[number];
|
||||
|
||||
export const PAYMENT_CURRENCY_OPTIONS: Array<{
|
||||
@@ -93,6 +93,11 @@ export const PAYMENT_CURRENCY_OPTIONS: Array<{
|
||||
label: "ETB",
|
||||
description: "Ethiopian Birr — local pricing and invoicing.",
|
||||
},
|
||||
{
|
||||
value: "DJF",
|
||||
label: "DJF",
|
||||
description: "Djibouti Franc — Djibouti-side pricing and invoicing.",
|
||||
},
|
||||
];
|
||||
|
||||
// one_time → ContractKind.OneTime; general_contract → ContractKind.General.
|
||||
|
||||
@@ -101,7 +101,7 @@ const shipmentFormBase = z.object({
|
||||
// The contract quotes in USD; the customer picks the billing currency for
|
||||
// THIS shipment. Starts empty so the choice is deliberate — validated as
|
||||
// required below. Intercity is forced to ETB (server-enforced too).
|
||||
paymentCurrency: z.enum(["USD", "ETB", ""]).default(""),
|
||||
paymentCurrency: z.enum(["USD", "ETB", "DJF", ""]).default(""),
|
||||
// Container contracts only: return the empty container(s) to EDR after
|
||||
// unloading. Seeded from the contract's equipment return; bulk ignores it.
|
||||
withReturn: z.boolean().default(false),
|
||||
|
||||
Reference in New Issue
Block a user