Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 30 additions & 5 deletions app/api/v1/recipients/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,31 @@ export const POST = withRateLimit(async (request: NextRequest) => {
);
}

const trimmedInstitutionCode = String(institutionCode).trim();

// NUBAN is 10 digits; SAFAKEPC uses 6 digits - reject invalid length so we don't save 11-digit (e.g. phone) as beneficiary
const digits = String(accountIdentifier).replace(/\D/g, "");
const requiredLen = trimmedInstitutionCode === "SAFAKEPC" ? 6 : 10;
if (digits.length !== requiredLen) {
trackApiError(
request,
"/api/v1/recipients",
"POST",
new Error("Invalid account identifier length"),
400,
);
return NextResponse.json(
{
success: false,
error:
requiredLen === 10
? "Please enter a valid 10-digit account number."
: "Please enter a valid 6-digit account number.",
},
{ status: 400 },
);
}

// Validate type
if (!["bank", "mobile_money"].includes(type)) {
trackApiError(
Expand Down Expand Up @@ -199,7 +224,7 @@ export const POST = withRateLimit(async (request: NextRequest) => {
}
}

// Insert recipient (upsert on unique constraint)
// Insert recipient (upsert on unique constraint) - store sanitized digits so DB has consistent format
const { data, error } = await supabaseAdmin
.from("saved_recipients")
.upsert(
Expand All @@ -208,8 +233,8 @@ export const POST = withRateLimit(async (request: NextRequest) => {
normalized_wallet_address: walletAddress,
name: name.trim(),
institution: institution.trim(),
institution_code: institutionCode.trim(),
account_identifier: accountIdentifier.trim(),
institution_code: trimmedInstitutionCode,
account_identifier: digits,
type,
},
{
Expand Down Expand Up @@ -244,14 +269,14 @@ export const POST = withRateLimit(async (request: NextRequest) => {
const responseTime = Date.now() - startTime;
trackApiResponse("/api/v1/recipients", "POST", 200, responseTime, {
wallet_address: walletAddress,
institution_code: institutionCode,
institution_code: trimmedInstitutionCode,
type,
});

// Track business event
trackBusinessEvent("Recipient Saved", {
wallet_address: walletAddress,
institution_code: institutionCode,
institution_code: trimmedInstitutionCode,
type,
});

Expand Down
41 changes: 29 additions & 12 deletions app/components/recipient/RecipientDetailsForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -243,20 +243,29 @@ export const RecipientDetailsForm = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedInstitution, isManualEntry]);

// Fetch recipient name based on institution and account identifier
// Fetch recipient name based on institution and account identifier (only when exactly 10-digit NUBAN or 6-digit SAFAKEPC)
useEffect(() => {
let timeoutId: NodeJS.Timeout;
const getRecipientName = async () => {
if (!isManualEntry) return;

if (
!institution ||
!accountIdentifier ||
accountIdentifier.toString().length <
(selectedInstitution?.code === "SAFAKEPC" ? 6 : 10)
)
const digits = String(accountIdentifier ?? "").replace(/\D/g, "");
const requiredLen = selectedInstitution?.code === "SAFAKEPC" ? 6 : 10;

if (!institution || !accountIdentifier || digits.length !== requiredLen) {
if (accountIdentifier && digits.length > 0 && digits.length !== requiredLen) {
setRecipientNameError(
requiredLen === 10
? "Please enter a valid 10-digit account Number."
: "Invalid account number. Please enter a 6-digit account number.",
);
} else {
setRecipientNameError("");
}
return;
}

setRecipientNameError("");
setIsFetchingRecipientName(true);
setValue("recipientName", "");

Expand Down Expand Up @@ -380,19 +389,27 @@ export const RecipientDetailsForm = ({
</button>
</div>

{/* Account number */}
{/* Account number - NUBAN is 10 digits; SAFAKEPC uses 6 digits */}
<div className="w-full flex-1 flex-shrink-0 sm:w-1/2">
<input
type="number"
type="text"
inputMode="numeric"
placeholder="Account number"
maxLength={selectedInstitution?.code === "SAFAKEPC" ? 6 : 10}
{...register("accountIdentifier", {
required: {
value: true,
message: "Account number is required",
},
minLength: {
value: selectedInstitution?.code === "SAFAKEPC" ? 6 : 10,
message: "Account number is invalid",
validate: (value) => {
const digits = String(value ?? "").replace(/\D/g, "");
const requiredLen = selectedInstitution?.code === "SAFAKEPC" ? 6 : 10;
if (digits.length !== requiredLen) {
return requiredLen === 10
? "Please enter a valid 10-digit account Number."
: "Invalid account number. Please enter a 6-digit account number.";
}
return true;
},
onChange: () => setIsManualEntry(true),
})}
Expand Down