Skip to content

fix(merchant): separate contact OTP from user login, add KYB workflow…#21

Open
JoesWalker wants to merge 1 commit into
soumen0818:mainfrom
JoesWalker:fix/issue-11-merchant-kyb-contact-verification
Open

fix(merchant): separate contact OTP from user login, add KYB workflow…#21
JoesWalker wants to merge 1 commit into
soumen0818:mainfrom
JoesWalker:fix/issue-11-merchant-kyb-contact-verification

Conversation

@JoesWalker

@JoesWalker JoesWalker commented Jun 29, 2026

Copy link
Copy Markdown

PR Description:

closes #11

Problem

MerchantRegistrationScreen was calling supabase.auth.signInWithOtp({ email: businessEmail }) to verify the merchant contact email. This replaced the
wallet owner's active Supabase session with the merchant contact email session, which could assign the merchants row to the wrong auth_user_id. The
merchants table also had no KYB/verification workflow beyond is_active.

Changes

Database (App/supabase_schema.sql)

  • Added KYB columns to merchants: verification_status (pending / approved / rejected), verified_contact_email, contact_email_verified,
    submitted_at, reviewed_at, rejection_reason
  • New merchant_contact_verifications table audits OTP send/verify events without touching auth sessions
  • Updated get_own_merchant_by_wallet RPC to return all new columns
  • RLS policies, indexes, and updated_at trigger for the new table

Relayer (relayer-service/server.js)

  • POST /merchants/send-contact-otp — sends email OTP via Supabase Admin API server-side. The caller's auth session is never changed. Validates
    wallet owner via auth_user_id.
  • POST /merchants/verify-contact-otp — verifies OTP server-side. On success patches contact_email_verified = true and auto-approves the merchant
    when PILOT_MODE=true.

App services

  • auth.ts: Added sendMerchantContactOtp and verifyMerchantContactOtp — both call the relayer with the current session bearer token. No
    signInWithOtp calls.
  • merchant.ts: Extended Merchant interface with all KYB fields. registerAsMerchant sets verification_status = 'approved' in pilot mode,
    'pending' in production.

Screens

  • MerchantRegistrationScreen: Creates a draft merchant row (is_active=false) before sending OTP so merchantId can be passed to the relayer. On
    "Register", updates the draft row instead of re-inserting (avoids UNIQUE wallet_address violation). Session is never replaced.
  • MerchantDashboardScreen: Shows pending / approved / rejected status banner above the Business status section.
  • MerchantQRGeneratorScreen and MerchantGlobalQRScreen: Block QR generation for unapproved merchants with an alert + navigate back.

Pilot mode safety

PILOT_MODE=true (relayer default) auto-approves merchants on OTP verify, so the existing pilot flow is unchanged. Production (PILOT_MODE=false)
leaves merchants in pending state for admin review.

Verification

  • npx tsc --noEmit — 0 errors
  • node --check relayer-service/server.js — 0 errors
  • After OTP verify: supabase.auth.currentUser is still the wallet owner, not the merchant contact email
  • merchants.auth_user_id always matches the original wallet owner's auth.uid()
  • Unapproved merchants cannot open QR screens or use the relayer contract registration

Summary by CodeRabbit

  • New Features

    • Added merchant verification status visibility in the dashboard, including under review, rejected, and approved messages.
    • Added a new merchant contact email verification flow during registration.
  • Bug Fixes

    • Prevented QR code access and generation for merchants that are not yet approved.
    • Improved registration handling so draft merchant details are preserved after email verification.
  • Backend/Platform

    • Added support for tracking merchant verification progress and contact-email verification across the app.

…soumen0818#11)

- Replace signInWithOtp merchant email flow with relayer Admin API endpoints
  POST /merchants/send-contact-otp and POST /merchants/verify-contact-otp so
  the wallet owner's Supabase auth session is never replaced by the merchant
  contact email.

- Add KYB columns to merchants table: verification_status (pending/approved/
  rejected), verified_contact_email, contact_email_verified, submitted_at,
  reviewed_at, rejection_reason.

- Add merchant_contact_verifications audit table to track OTP send/verify
  events without touching auth sessions.

- Update get_own_merchant_by_wallet RPC to return new KYB columns.

- Add RLS policies, indexes, and updated_at trigger for the new table.

- MerchantRegistrationScreen: create draft merchant row (is_active=false)
  before sending OTP so merchantId can be passed to the relayer. On Register,
  update the draft row instead of re-inserting (avoids UNIQUE wallet_address
  constraint violation).

- Merchant type and registerAsMerchant: include all KYB fields; auto-approve
  in pilot mode (EXPO_PUBLIC_PILOT_MODE=true), leave pending in production.

- auth.ts: sendMerchantContactOtp and verifyMerchantContactOtp call the
  relayer with the current session bearer token. No signInWithOtp calls.

- MerchantDashboardScreen: show pending/approved/rejected status banner.

- MerchantQRGeneratorScreen and MerchantGlobalQRScreen: block QR generation
  for unapproved merchants with an alert and navigation back.

- Pilot mode: PILOT_MODE=true on the relayer auto-approves on OTP verify, so
  existing pilot UX is unchanged. Production defaults to pending review.

Fixes soumen0818#11
@drips-wave

drips-wave Bot commented Jun 29, 2026

Copy link
Copy Markdown

@JoesWalker Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a merchant KYB (Know Your Business) verification workflow. The database schema gains verification status fields and a merchant_contact_verifications table. A relayer service exposes new OTP endpoints that verify merchant contact email without touching the wallet owner's auth session. The registration screen switches to a draft-merchant flow, and the dashboard and QR screens gate access based on approval status.

Changes

Merchant KYB & Contact OTP Flow

Layer / File(s) Summary
DB schema: KYB columns, verifications table, RLS, function
App/supabase_schema.sql
Adds verification_status and related KYB columns to merchants, creates merchant_contact_verifications table with indexes and RLS policies, extends get_own_merchant_by_wallet return shape, and adds an updated_at trigger.
Merchant service: KYB fields and registerAsMerchant updates
App/src/services/merchant.ts
Extends Merchant interface with KYB optional fields, persists verification_status/submitted_at based on PILOT_MODE, and exposes verificationStatus in the return payload.
Relayer: send/verify contact OTP endpoints and helpers
relayer-service/server.js
Adds POST /merchants/send-contact-otp and POST /merchants/verify-contact-otp handlers with ownership checks, Supabase Admin OTP calls, audit upserts, and auto-approval in PILOT_MODE; adds validation/persistence helpers.
Auth service: sendMerchantContactOtp / verifyMerchantContactOtp
App/src/services/auth.ts
Exports two new functions that read the active Supabase session token and POST to relayer OTP endpoints without altering the auth session; existing OTP functions are unchanged.
Registration screen: draft-merchant OTP flow and handleRegister overhaul
App/src/screens/MerchantRegistrationScreen.tsx
Replaces sendEmailOTP/verifyEmailOTP with merchant-contact OTP functions, introduces pendingMerchantId draft state with email-change reset, reworks handleSendEmailOTP with preflight validation and draft creation, and overhauls handleRegister to activate the draft row or fall back to full registration.
Dashboard KYB banner and QR screen approval gating
App/src/screens/MerchantDashboardScreen.tsx, App/src/screens/MerchantGlobalQRScreen.tsx, App/src/screens/MerchantQRGeneratorScreen.tsx
Dashboard renders a conditional KYB status banner (pending/rejected/approved). QR screens block unapproved merchants with a status-specific alert and navigate back.

Sequence Diagram(s)

sequenceDiagram
  participant App as MerchantRegistrationScreen
  participant AuthSvc as auth.ts
  participant Relayer as relayer-service
  participant SupabaseAdmin
  participant DB as Supabase DB

  App->>DB: registerAsMerchant (inactive draft, status=pending)
  App->>AuthSvc: sendMerchantContactOtp(merchantId, email)
  AuthSvc->>Relayer: POST /merchants/send-contact-otp (Bearer token)
  Relayer->>DB: verify merchant ownership
  Relayer->>SupabaseAdmin: POST /auth/v1/otp {email}
  Relayer->>DB: upsert merchant_contact_verifications
  Relayer-->>AuthSvc: {success}
  AuthSvc-->>App: {success}

  App->>AuthSvc: verifyMerchantContactOtp(merchantId, email, token)
  AuthSvc->>Relayer: POST /merchants/verify-contact-otp (Bearer token)
  Relayer->>SupabaseAdmin: POST /auth/v1/verify {email, token}
  Relayer->>DB: update merchants.verification_status (approved if PILOT_MODE)
  Relayer->>DB: patch merchant_contact_verifications
  Relayer-->>AuthSvc: {success, verificationStatus}
  AuthSvc-->>App: {success, verificationStatus}

  App->>DB: UPDATE merchants SET is_active=true (activate draft)
  App->>App: syncMerchantContract → navigate MerchantDashboard
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 A merchant hops in, but must first be known,
Their email verified, their status shown.
No QR for the pending, no code for the banned,
The relayer checks tokens with a careful hand.
Approved at last — the dashboard glows green,
The tidiest KYB a rabbit has seen! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 68.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: merchant OTP separation and KYB workflow.
Linked Issues check ✅ Passed The changes match issue #11 by separating contact OTP from login, adding KYB fields, and blocking QR access before approval.
Out of Scope Changes check ✅ Passed All changes support merchant OTP separation and KYB approval gating; no unrelated edits stand out.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
App/src/screens/MerchantRegistrationScreen.tsx (1)

80-85: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Don't drop the draft merchant id when the email changes.

Once the first OTP send creates a draft row, clearing pendingMerchantId here turns the next "Send code" into another registerAsMerchant() insert. App/src/services/merchant.ts:212-280 does a plain merchants insert, so correcting the email after the first send will hit the unique wallet_address constraint instead of reusing the existing draft.

Suggested fix
   const handleEmailChange = (text: string) => {
     setEmail(text);
     if (emailVerified) {
       setEmailVerified(false);
     }
-    setPendingMerchantId('');
     setEmailOTP('');
     setEmailOTPError('');
   };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@App/src/screens/MerchantRegistrationScreen.tsx` around lines 80 - 85, The
email change handler in MerchantRegistrationScreen is incorrectly clearing the
draft merchant id, which causes later OTP sends to create a new draft instead of
updating the existing one. Update handleEmailChange so it only resets the
email/verification state and preserves pendingMerchantId, and keep the resend
flow in sync with the existing draft created by registerAsMerchant in
merchant.ts.
🧹 Nitpick comments (1)
App/src/services/auth.ts (1)

331-347: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Retire or relabel the legacy merchant-registration OTP helper.

Line 332 still documents sendEmailOTP as “for merchant registration”, but it calls supabase.auth.signInWithOtp, the session-changing flow this PR is replacing. Remove it if unused, or rename/comment it as login-only so merchant registration keeps using sendMerchantContactOtp.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@App/src/services/auth.ts` around lines 331 - 347, The legacy sendEmailOTP
helper is mislabeled as merchant-registration OTP even though it uses
supabase.auth.signInWithOtp, which is the login flow. Update sendEmailOTP in
auth.ts to either remove it if nothing references it or relabel/rename its
comment and purpose to login-only, and ensure merchant registration continues to
use sendMerchantContactOtp instead.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@App/src/screens/MerchantDashboardScreen.tsx`:
- Around line 206-238: The KYB banner only shows review state, but the
QR-readiness copy/actions in MerchantDashboardScreen still rely on
contractSynced alone. Update the readiness logic in MerchantDashboardScreen so
any “ready/active” messaging and QR-related actions are gated on
verificationStatus === 'approved' in addition to contractSynced, and keep
pending/rejected merchants in the blocked state even if the contract is synced.
Reference the existing verificationStatus banner conditions and the
contractSynced-based readiness checks to make the change consistently.

In `@App/src/screens/MerchantRegistrationScreen.tsx`:
- Around line 164-193: The OTP preflight path in MerchantRegistrationScreen is
incorrectly calling registerAsMerchant, which performs full submission, contract
registration, caching, and merchantRegistered side effects instead of creating a
draft. Replace this with a draft-specific helper for the merchant row creation
branch that only inserts/updates the minimal pending record needed to obtain
merchantId, and keep registerAsMerchant and syncMerchantContract reserved for
the final registration flow after OTP verification/approval. Ensure the new
helper is used in the merchant draft branch and that pending state updates still
happen without marking the device as fully registered.

In `@App/src/services/merchant.ts`:
- Around line 227-239: The merchant creation flow in the insert block currently
defaults to approved when EXPO_PUBLIC_PILOT_MODE is unset, which can promote
draft merchants too early. Update the verificationStatus logic in merchant.ts to
fail closed: only auto-approve when pilot mode is explicitly enabled, otherwise
keep new merchants pending. Then make sure the downstream contract
sync/cache/event section in this flow only runs after both approval and contact
verification are true, including the path triggered by handleSendEmailOTP so
draft records stay pending until OTP verification completes.

In `@App/supabase_schema.sql`:
- Line 61: The contact verification table currently allows unscoped OTP audit
rows because merchant_id is nullable. Update the schema definition for the
contact verification row/OTP flow to make merchant_id required and keep the
foreign key to merchants(id), then ensure any insert/create path that writes
these rows always supplies a merchant_id so every verification record is tied to
a merchant.
- Around line 177-183: The idempotent merchants migration adds
verification_status but does not apply the allowed-value CHECK constraint, so
existing deployments can accept invalid statuses. Update the ALTER TABLE
migration for merchants to add the same verification_status constraint used in
CREATE TABLE, and make it safe to run repeatedly by referencing the merchants
verification_status column and its status validation consistently.
- Around line 574-592: The merchant_contact_verifications policies currently
allow authenticated users to insert and update their own rows, which exposes OTP
audit fields to direct client writes. Update the policy set in
merchant_contact_verifications so clients remain read-only by keeping only the
SELECT policy for auth.uid()-owned rows, and remove or restrict the
INSERT/UPDATE policies so only the service role/relayer path can write
verified_at and is_verified state. Also adjust the GRANT on
merchant_contact_verifications to match the new read-only client access, using
the existing policy names as the place to locate the change.

In `@relayer-service/server.js`:
- Around line 266-315: The /merchants/send-contact-otp handler currently sends
OTPs without any server-side cooldown, so add a throttle check before the
Supabase Auth request. In the app.post route, use fetchMerchantForOwner plus
merchant_contact_verifications.otp_sent_at to enforce a per-(authUserId,
merchantId, contactEmail) cooldown, and reject requests that are too soon with a
429-style response. After a successful send, continue updating the record via
upsertMerchantContactVerification so the timestamp is persisted for future
checks.
- Around line 1186-1189: The auto-approval path in the verification update logic
leaves reviewed_at unset when newVerificationStatus becomes approved. Update the
merchant patching flow around the newVerificationStatus block to stamp
reviewed_at at the same time as the pilot auto-approval timestamp, alongside
submitted_at, so the approval metadata is complete.
- Around line 285-299: The OTP request in server.js is hardcoded with
create_user: false, which causes first-time merchant contact emails to fail with
a signup-not-allowed error. Update the auth/v1/otp flow in the surrounding
adminRes fetch block so it can create the Supabase auth identity for new
contactEmail values, either by setting create_user: true or switching to the
appropriate server-side OTP path. Keep the existing request structure in the
fetch call, but ensure the chosen behavior supports first-time inboxes while
preserving the current contactEmail-based flow.

---

Outside diff comments:
In `@App/src/screens/MerchantRegistrationScreen.tsx`:
- Around line 80-85: The email change handler in MerchantRegistrationScreen is
incorrectly clearing the draft merchant id, which causes later OTP sends to
create a new draft instead of updating the existing one. Update
handleEmailChange so it only resets the email/verification state and preserves
pendingMerchantId, and keep the resend flow in sync with the existing draft
created by registerAsMerchant in merchant.ts.

---

Nitpick comments:
In `@App/src/services/auth.ts`:
- Around line 331-347: The legacy sendEmailOTP helper is mislabeled as
merchant-registration OTP even though it uses supabase.auth.signInWithOtp, which
is the login flow. Update sendEmailOTP in auth.ts to either remove it if nothing
references it or relabel/rename its comment and purpose to login-only, and
ensure merchant registration continues to use sendMerchantContactOtp instead.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 914cc4c9-f517-4e1e-85a2-231893958b60

📥 Commits

Reviewing files that changed from the base of the PR and between 1ba3f0f and a17a668.

📒 Files selected for processing (8)
  • App/src/screens/MerchantDashboardScreen.tsx
  • App/src/screens/MerchantGlobalQRScreen.tsx
  • App/src/screens/MerchantQRGeneratorScreen.tsx
  • App/src/screens/MerchantRegistrationScreen.tsx
  • App/src/services/auth.ts
  • App/src/services/merchant.ts
  • App/supabase_schema.sql
  • relayer-service/server.js

Comment on lines +206 to +238
{/* KYB / verification status banner */}
{verificationStatus === 'pending' && (
<View style={[styles.kybBanner, styles.kybPending]}>
<Ionicons name="time-outline" size={18} color={COLORS.warning} />
<View style={styles.kybBannerText}>
<Text style={[styles.kybBannerTitle, { color: COLORS.warning }]}>Verification under review</Text>
<Text style={styles.kybBannerSubtitle}>
Your merchant account is being reviewed. QR payments will be enabled once approved.
</Text>
</View>
</View>
)}
{verificationStatus === 'rejected' && (
<View style={[styles.kybBanner, styles.kybRejected]}>
<Ionicons name="close-circle-outline" size={18} color={COLORS.error} />
<View style={styles.kybBannerText}>
<Text style={[styles.kybBannerTitle, { color: COLORS.error }]}>Verification rejected</Text>
<Text style={styles.kybBannerSubtitle}>
{rejectionReason || 'Your application was not approved. Please contact support.'}
</Text>
</View>
</View>
)}
{verificationStatus === 'approved' && (
<View style={[styles.kybBanner, styles.kybApproved]}>
<Ionicons name="shield-checkmark-outline" size={18} color={COLORS.success} />
<View style={styles.kybBannerText}>
<Text style={[styles.kybBannerTitle, { color: COLORS.success }]}>Account approved</Text>
<Text style={styles.kybBannerSubtitle}>Your merchant account is active and ready to accept payments.</Text>
</View>
</View>
)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The new KYB banner still leaves pending merchants looking QR-ready.

The banner says review is pending/rejected, but the screen below still drives merchant/QR readiness from contractSynced only. With the current registration flow, a non-approved merchant can already have contractSynced === true, so this screen can still present them as ready even though QR access is blocked elsewhere. Gate the readiness copy/actions off verificationStatus === 'approved' too.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@App/src/screens/MerchantDashboardScreen.tsx` around lines 206 - 238, The KYB
banner only shows review state, but the QR-readiness copy/actions in
MerchantDashboardScreen still rely on contractSynced alone. Update the readiness
logic in MerchantDashboardScreen so any “ready/active” messaging and QR-related
actions are gated on verificationStatus === 'approved' in addition to
contractSynced, and keep pending/rejected merchants in the blocked state even if
the contract is synced. Reference the existing verificationStatus banner
conditions and the contractSynced-based readiness checks to make the change
consistently.

Comment on lines +164 to +193
if (!merchantId) {
// Create a minimal draft merchant row so we can pass merchantId to the
// relayer. The row will be completed (logo, address, etc.) when the user
// taps "Register" after OTP verification. We mark it inactive until done.
const finalCategory = category === 'other' ? customCategory : category;
const normalizedPhone = normalizeMerchantPhoneInput(phoneNumber);

const draftResult = await registerAsMerchant({
business_name: businessName.trim(),
wallet_address: walletAddress,
owner_name: ownerName.trim(),
email: normalizedEmail,
phone_number: normalizedPhone || undefined,
business_address: businessAddress.trim() || undefined,
business_registration_number: businessRegistrationNumber.trim() || undefined,
description: description.trim() || undefined,
category: finalCategory || undefined,
logo_url: 'default-merchant-logo',
is_active: false, // inactive until fully confirmed
});

if (!draftResult.success || !draftResult.merchantId) {
setEmailOTPLoading(false);
AlertManager.alert('Error', draftResult.error || 'Could not create merchant draft. Please try again.');
return;
}

merchantId = draftResult.merchantId;
setPendingMerchantId(merchantId);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Use a draft-specific create path instead of registerAsMerchant here.

registerAsMerchant in App/src/services/merchant.ts:212-280 does far more than create a row: it sets submitted_at/verification_status, calls registerContractMerchant, caches local merchant state, and emits merchantRegistered. Using it for the OTP preflight means tapping "Send code" can submit an incomplete merchant, mark the device as merchant-capable before OTP succeeds, and perform one on-chain registration; the draft flow then calls syncMerchantContract again on Lines 356-357. This needs a draft-only insert/update helper that skips submission and contract-sync side effects until final registration/approval.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@App/src/screens/MerchantRegistrationScreen.tsx` around lines 164 - 193, The
OTP preflight path in MerchantRegistrationScreen is incorrectly calling
registerAsMerchant, which performs full submission, contract registration,
caching, and merchantRegistered side effects instead of creating a draft.
Replace this with a draft-specific helper for the merchant row creation branch
that only inserts/updates the minimal pending record needed to obtain
merchantId, and keep registerAsMerchant and syncMerchantContract reserved for
the final registration flow after OTP verification/approval. Ensure the new
helper is used in the merchant draft branch and that pending state updates still
happen without marking the device as fully registered.

Comment on lines +227 to +239
const isPilotMode = process.env.EXPO_PUBLIC_PILOT_MODE !== 'false';
// In pilot mode, registration is auto-approved. In production, leave as pending
// until an admin reviews the KYB submission.
const verificationStatus = isPilotMode ? 'approved' : 'pending';

const { data, error } = await supabase
.from('merchants')
.insert({
...merchant,
auth_user_id: authUserId,
cpay_id: cpayId, // Save C-Pay ID to database
cpay_id: cpayId,
verification_status: verificationStatus,
submitted_at: new Date().toISOString(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Fail closed and keep draft merchants pending until OTP verification.

EXPO_PUBLIC_PILOT_MODE !== 'false' auto-approves when the env var is missing, and handleSendEmailOTP creates an inactive draft through this function before OTP verification. That draft can be stored as approved and proceed into contract sync/cache/event below. Force pilot mode to be explicit, keep drafts pending, and only sync contracts after approval plus contact verification.

Proposed fix
-    const isPilotMode = process.env.EXPO_PUBLIC_PILOT_MODE !== 'false';
+    const isPilotMode = process.env.EXPO_PUBLIC_PILOT_MODE === 'true';
+    const isDraft = merchant.is_active === false;
     // In pilot mode, registration is auto-approved. In production, leave as pending
     // until an admin reviews the KYB submission.
-    const verificationStatus = isPilotMode ? 'approved' : 'pending';
+    const verificationStatus = !isDraft && isPilotMode ? 'approved' : 'pending';
 
     const { data, error } = await supabase
       .from('merchants')
       .insert({
         ...merchant,
         auth_user_id: authUserId,
         cpay_id: cpayId,
         verification_status: verificationStatus,
-        submitted_at: new Date().toISOString(),
+        submitted_at: isDraft ? null : new Date().toISOString(),
       })
       .select()
       .single();
+
+    if (isDraft) {
+      return {
+        success: true,
+        merchantId: data.id,
+        contractSynced: false,
+        contractStatus: 'draft',
+        verificationStatus: data.verification_status || verificationStatus,
+      };
+    }

Then gate the existing contract sync block with:

+    const canSyncContract =
+      data.verification_status === 'approved' &&
+      data.contact_email_verified === true;
+
-    try {
+    if (canSyncContract) {
+      try {
         const contractResult = await registerContractMerchant(data.id, merchant.wallet_address);
         contractSynced = true;
         contractStatus = contractResult.contractStatus || contractResult.status || 'synced';
-    } catch (contractSyncError: any) {
-      contractError = contractSyncError?.message || 'Contract merchant registration failed';
-      console.warn('Merchant saved but contract sync failed:', contractError);
+      } catch (contractSyncError: any) {
+        contractError = contractSyncError?.message || 'Contract merchant registration failed';
+        console.warn('Merchant saved but contract sync failed:', contractError);
+      }
+    } else {
+      contractStatus = 'pending_kyb';
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@App/src/services/merchant.ts` around lines 227 - 239, The merchant creation
flow in the insert block currently defaults to approved when
EXPO_PUBLIC_PILOT_MODE is unset, which can promote draft merchants too early.
Update the verificationStatus logic in merchant.ts to fail closed: only
auto-approve when pilot mode is explicitly enabled, otherwise keep new merchants
pending. Then make sure the downstream contract sync/cache/event section in this
flow only runs after both approval and contact verification are true, including
the path triggered by handleSendEmailOTP so draft records stay pending until OTP
verification completes.

Comment thread App/supabase_schema.sql
CREATE TABLE IF NOT EXISTS merchant_contact_verifications (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
auth_user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
merchant_id UUID REFERENCES merchants(id) ON DELETE CASCADE,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Require merchant_id for contact verification rows.

The OTP flow is merchant-scoped, but nullable merchant_id allows unscoped audit rows that cannot be reliably tied back to a merchant.

Proposed fix
-    merchant_id UUID REFERENCES merchants(id) ON DELETE CASCADE,
+    merchant_id UUID NOT NULL REFERENCES merchants(id) ON DELETE CASCADE,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
merchant_id UUID REFERENCES merchants(id) ON DELETE CASCADE,
merchant_id UUID NOT NULL REFERENCES merchants(id) ON DELETE CASCADE,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@App/supabase_schema.sql` at line 61, The contact verification table currently
allows unscoped OTP audit rows because merchant_id is nullable. Update the
schema definition for the contact verification row/OTP flow to make merchant_id
required and keep the foreign key to merchants(id), then ensure any
insert/create path that writes these rows always supplies a merchant_id so every
verification record is tied to a merchant.

Comment thread App/supabase_schema.sql
Comment on lines +177 to +183
-- KYB / contact verification columns (idempotent for existing projects)
ALTER TABLE merchants ADD COLUMN IF NOT EXISTS verification_status TEXT NOT NULL DEFAULT 'pending';
ALTER TABLE merchants ADD COLUMN IF NOT EXISTS verified_contact_email TEXT;
ALTER TABLE merchants ADD COLUMN IF NOT EXISTS contact_email_verified BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE merchants ADD COLUMN IF NOT EXISTS submitted_at TIMESTAMPTZ;
ALTER TABLE merchants ADD COLUMN IF NOT EXISTS reviewed_at TIMESTAMPTZ;
ALTER TABLE merchants ADD COLUMN IF NOT EXISTS rejection_reason TEXT;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Apply the status CHECK constraint in the idempotent migration too.

New installs get the verification_status constraint from CREATE TABLE, but existing deployments only get the column from ALTER TABLE, without the allowed-value constraint.

Proposed fix
 ALTER TABLE merchants ADD COLUMN IF NOT EXISTS verification_status TEXT NOT NULL DEFAULT 'pending';
 ALTER TABLE merchants ADD COLUMN IF NOT EXISTS verified_contact_email TEXT;
 ALTER TABLE merchants ADD COLUMN IF NOT EXISTS contact_email_verified BOOLEAN NOT NULL DEFAULT FALSE;
 ALTER TABLE merchants ADD COLUMN IF NOT EXISTS submitted_at TIMESTAMPTZ;
 ALTER TABLE merchants ADD COLUMN IF NOT EXISTS reviewed_at TIMESTAMPTZ;
 ALTER TABLE merchants ADD COLUMN IF NOT EXISTS rejection_reason TEXT;
+
+DO $$
+BEGIN
+  IF NOT EXISTS (
+    SELECT 1
+    FROM pg_constraint
+    WHERE conname = 'merchants_verification_status_check'
+  ) THEN
+    ALTER TABLE merchants
+      ADD CONSTRAINT merchants_verification_status_check
+      CHECK (verification_status IN ('pending', 'approved', 'rejected'));
+  END IF;
+END $$;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
-- KYB / contact verification columns (idempotent for existing projects)
ALTER TABLE merchants ADD COLUMN IF NOT EXISTS verification_status TEXT NOT NULL DEFAULT 'pending';
ALTER TABLE merchants ADD COLUMN IF NOT EXISTS verified_contact_email TEXT;
ALTER TABLE merchants ADD COLUMN IF NOT EXISTS contact_email_verified BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE merchants ADD COLUMN IF NOT EXISTS submitted_at TIMESTAMPTZ;
ALTER TABLE merchants ADD COLUMN IF NOT EXISTS reviewed_at TIMESTAMPTZ;
ALTER TABLE merchants ADD COLUMN IF NOT EXISTS rejection_reason TEXT;
-- KYB / contact verification columns (idempotent for existing projects)
ALTER TABLE merchants ADD COLUMN IF NOT EXISTS verification_status TEXT NOT NULL DEFAULT 'pending';
ALTER TABLE merchants ADD COLUMN IF NOT EXISTS verified_contact_email TEXT;
ALTER TABLE merchants ADD COLUMN IF NOT EXISTS contact_email_verified BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE merchants ADD COLUMN IF NOT EXISTS submitted_at TIMESTAMPTZ;
ALTER TABLE merchants ADD COLUMN IF NOT EXISTS reviewed_at TIMESTAMPTZ;
ALTER TABLE merchants ADD COLUMN IF NOT EXISTS rejection_reason TEXT;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'merchants_verification_status_check'
) THEN
ALTER TABLE merchants
ADD CONSTRAINT merchants_verification_status_check
CHECK (verification_status IN ('pending', 'approved', 'rejected'));
END IF;
END $$;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@App/supabase_schema.sql` around lines 177 - 183, The idempotent merchants
migration adds verification_status but does not apply the allowed-value CHECK
constraint, so existing deployments can accept invalid statuses. Update the
ALTER TABLE migration for merchants to add the same verification_status
constraint used in CREATE TABLE, and make it safe to run repeatedly by
referencing the merchants verification_status column and its status validation
consistently.

Comment thread App/supabase_schema.sql
Comment on lines +574 to +592
-- merchant_contact_verifications: wallet owner can read/write their own rows.
-- The relayer uses the service-role key to write verified_at/is_verified.
DROP POLICY IF EXISTS "merchant_contact_verifications_select_own" ON merchant_contact_verifications;
CREATE POLICY "merchant_contact_verifications_select_own" ON merchant_contact_verifications
FOR SELECT
USING (auth.uid() IS NOT NULL AND auth_user_id = auth.uid());

DROP POLICY IF EXISTS "merchant_contact_verifications_insert_own" ON merchant_contact_verifications;
CREATE POLICY "merchant_contact_verifications_insert_own" ON merchant_contact_verifications
FOR INSERT
WITH CHECK (auth.uid() IS NOT NULL AND auth_user_id = auth.uid());

DROP POLICY IF EXISTS "merchant_contact_verifications_update_own" ON merchant_contact_verifications;
CREATE POLICY "merchant_contact_verifications_update_own" ON merchant_contact_verifications
FOR UPDATE
USING (auth.uid() IS NOT NULL AND auth_user_id = auth.uid())
WITH CHECK (auth.uid() IS NOT NULL AND auth_user_id = auth.uid());

GRANT SELECT, INSERT, UPDATE ON merchant_contact_verifications TO authenticated;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not let clients write OTP audit/verification rows directly.

The app now uses relayer endpoints for OTP, but these policies still allow any authenticated owner to insert or update their own is_verified / verified_at rows directly. Keep client access read-only and let the service role write audit state.

Proposed fix
 -- merchant_contact_verifications: wallet owner can read/write their own rows.
 -- The relayer uses the service-role key to write verified_at/is_verified.
 DROP POLICY IF EXISTS "merchant_contact_verifications_select_own" ON merchant_contact_verifications;
 CREATE POLICY "merchant_contact_verifications_select_own" ON merchant_contact_verifications
 FOR SELECT
 USING (auth.uid() IS NOT NULL AND auth_user_id = auth.uid());
 
 DROP POLICY IF EXISTS "merchant_contact_verifications_insert_own" ON merchant_contact_verifications;
-CREATE POLICY "merchant_contact_verifications_insert_own" ON merchant_contact_verifications
-FOR INSERT
-WITH CHECK (auth.uid() IS NOT NULL AND auth_user_id = auth.uid());
 
 DROP POLICY IF EXISTS "merchant_contact_verifications_update_own" ON merchant_contact_verifications;
-CREATE POLICY "merchant_contact_verifications_update_own" ON merchant_contact_verifications
-FOR UPDATE
-USING (auth.uid() IS NOT NULL AND auth_user_id = auth.uid())
-WITH CHECK (auth.uid() IS NOT NULL AND auth_user_id = auth.uid());
 
-GRANT SELECT, INSERT, UPDATE ON merchant_contact_verifications TO authenticated;
+REVOKE INSERT, UPDATE ON merchant_contact_verifications FROM authenticated;
+GRANT SELECT ON merchant_contact_verifications TO authenticated;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
-- merchant_contact_verifications: wallet owner can read/write their own rows.
-- The relayer uses the service-role key to write verified_at/is_verified.
DROP POLICY IF EXISTS "merchant_contact_verifications_select_own" ON merchant_contact_verifications;
CREATE POLICY "merchant_contact_verifications_select_own" ON merchant_contact_verifications
FOR SELECT
USING (auth.uid() IS NOT NULL AND auth_user_id = auth.uid());
DROP POLICY IF EXISTS "merchant_contact_verifications_insert_own" ON merchant_contact_verifications;
CREATE POLICY "merchant_contact_verifications_insert_own" ON merchant_contact_verifications
FOR INSERT
WITH CHECK (auth.uid() IS NOT NULL AND auth_user_id = auth.uid());
DROP POLICY IF EXISTS "merchant_contact_verifications_update_own" ON merchant_contact_verifications;
CREATE POLICY "merchant_contact_verifications_update_own" ON merchant_contact_verifications
FOR UPDATE
USING (auth.uid() IS NOT NULL AND auth_user_id = auth.uid())
WITH CHECK (auth.uid() IS NOT NULL AND auth_user_id = auth.uid());
GRANT SELECT, INSERT, UPDATE ON merchant_contact_verifications TO authenticated;
-- merchant_contact_verifications: wallet owner can read/write their own rows.
-- The relayer uses the service-role key to write verified_at/is_verified.
DROP POLICY IF EXISTS "merchant_contact_verifications_select_own" ON merchant_contact_verifications;
CREATE POLICY "merchant_contact_verifications_select_own" ON merchant_contact_verifications
FOR SELECT
USING (auth.uid() IS NOT NULL AND auth_user_id = auth.uid());
DROP POLICY IF EXISTS "merchant_contact_verifications_insert_own" ON merchant_contact_verifications;
DROP POLICY IF EXISTS "merchant_contact_verifications_update_own" ON merchant_contact_verifications;
REVOKE INSERT, UPDATE ON merchant_contact_verifications FROM authenticated;
GRANT SELECT ON merchant_contact_verifications TO authenticated;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@App/supabase_schema.sql` around lines 574 - 592, The
merchant_contact_verifications policies currently allow authenticated users to
insert and update their own rows, which exposes OTP audit fields to direct
client writes. Update the policy set in merchant_contact_verifications so
clients remain read-only by keeping only the SELECT policy for auth.uid()-owned
rows, and remove or restrict the INSERT/UPDATE policies so only the service
role/relayer path can write verified_at and is_verified state. Also adjust the
GRANT on merchant_contact_verifications to match the new read-only client
access, using the existing policy names as the place to locate the change.

Comment thread relayer-service/server.js
Comment on lines +266 to +315
app.post('/merchants/send-contact-otp', requireAuthenticatedUser, async (req, res) => {
assertSupabasePersistenceConfigured('POST /merchants/send-contact-otp');

const merchantId = assertNonEmptyString(req.body.merchantId, 'merchantId');
const contactEmail = assertNonEmptyString(req.body.contactEmail, 'contactEmail').toLowerCase().trim();

if (!isValidEmail(contactEmail)) {
return res.status(400).json({ error: 'Invalid contactEmail', code: 'INVALID_EMAIL' });
}

// Verify the authenticated user actually owns this merchant record.
const authUserId = req.auth?.sub;
const merchant = await fetchMerchantForOwner(merchantId, authUserId);
if (!merchant) {
return res.status(403).json({ error: 'Merchant not found or access denied', code: 'MERCHANT_ACCESS_DENIED' });
}

// Use the Admin API to generate a one-time link / send OTP without opening a
// session on this server-side call.
const adminRes = await fetch(
`${config.supabaseUrl.replace(/\/$/, '')}/auth/v1/otp`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
apikey: config.supabaseServiceRoleKey,
Authorization: `Bearer ${config.supabaseServiceRoleKey}`,
},
body: JSON.stringify({
email: contactEmail,
create_user: false,
// data is ignored for OTP but passing options keeps schema happy
options: { shouldCreateUser: false },
}),
}
);

if (!adminRes.ok) {
const body = await adminRes.json().catch(() => ({}));
const msg = body?.msg || body?.message || body?.error_description || 'Failed to send OTP';
return res.status(502).json({ error: msg, code: 'OTP_SEND_FAILED' });
}

// Record that an OTP was requested so we can audit later.
await upsertMerchantContactVerification({
authUserId,
merchantId,
contactEmail,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Add a server-side OTP send throttle.

This relayer endpoint can be called directly by any authenticated merchant owner and does not use the app’s local OTP rate limiter. Enforce a cooldown per (authUserId, merchantId, contactEmail) using merchant_contact_verifications.otp_sent_at before calling Supabase Auth.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relayer-service/server.js` around lines 266 - 315, The
/merchants/send-contact-otp handler currently sends OTPs without any server-side
cooldown, so add a throttle check before the Supabase Auth request. In the
app.post route, use fetchMerchantForOwner plus
merchant_contact_verifications.otp_sent_at to enforce a per-(authUserId,
merchantId, contactEmail) cooldown, and reject requests that are too soon with a
429-style response. After a successful send, continue updating the record via
upsertMerchantContactVerification so the timestamp is persisted for future
checks.

Comment thread relayer-service/server.js
Comment on lines +285 to +299
const adminRes = await fetch(
`${config.supabaseUrl.replace(/\/$/, '')}/auth/v1/otp`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
apikey: config.supabaseServiceRoleKey,
Authorization: `Bearer ${config.supabaseServiceRoleKey}`,
},
body: JSON.stringify({
email: contactEmail,
create_user: false,
// data is ignored for OTP but passing options keeps schema happy
options: { shouldCreateUser: false },
}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant file and surrounding logic
git ls-files relayer-service/server.js
wc -l relayer-service/server.js
sed -n '240,340p' relayer-service/server.js

# Find related OTP/auth usage in the repo
rg -n "create_user|shouldCreateUser|/auth/v1/otp|verify-contact-otp|send-contact-otp|verification_status|reviewed_at" relayer-service -S

# Check for any local docs or comments about Supabase OTP behavior
rg -n "Supabase|OTP|auth/v1/otp|create_user" -S README.md relayer-service .github docs 2>/dev/null || true

Repository: soumen0818/C-Pay

Length of output: 18821


🌐 Web query:

Supabase Auth /auth/v1/otp create_user false non-existing email send OTP semantics

💡 Result:

In Supabase Auth, when calling the /auth/v1/otp endpoint with the create_user parameter set to false, the system intentionally prevents the automatic creation of a new user account if the provided email address or phone number does not already exist in the database [1][2]. The semantics of this configuration are as follows: 1. Verification of Existence: The service first checks whether the provided identity (email or phone) exists in the database for the given audience [2]. 2. Blocking New Signups: If the identifier is not found, the endpoint returns a 422 Unprocessable Entity error with the message "Signups not allowed for otp" [2]. 3. Intended Usage: This parameter is designed to allow developers to separate sign-up and login flows, specifically to prevent unauthorized or automatic account creation during a sign-in attempt [1]. Because the server checks for the existence of the user to determine whether it is allowed to create one, this behavior can create a side-channel that reveals whether an email or phone number is registered in the system [3]. If the request returns a success (200) or a different error, it implies the user exists; if it returns the "Signups not allowed for otp" error, it indicates the user does not exist [3][2]. In client-side SDKs, this behavior is exposed via the shouldCreateUser option in the signInWithOtp method [4][5]. Setting this option to false is the standard way to restrict account creation [4].

Citations:


Allow OTP delivery for first-time merchant contact emails.

create_user: false makes /auth/v1/otp return 422 "Signups not allowed for otp" when contactEmail is not already a Supabase auth user, so new business inboxes will fail this flow. Use an OTP path that can create the auth identity server-side, or set create_user: true if a contact-only auth record is acceptable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relayer-service/server.js` around lines 285 - 299, The OTP request in
server.js is hardcoded with create_user: false, which causes first-time merchant
contact emails to fail with a signup-not-allowed error. Update the auth/v1/otp
flow in the surrounding adminRes fetch block so it can create the Supabase auth
identity for new contactEmail values, either by setting create_user: true or
switching to the appropriate server-side OTP path. Keep the existing request
structure in the fetch call, but ensure the chosen behavior supports first-time
inboxes while preserving the current contactEmail-based flow.

Comment thread relayer-service/server.js
Comment on lines +1186 to +1189
if (newVerificationStatus) {
merchantPatch.verification_status = newVerificationStatus;
merchantPatch.submitted_at = now;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Stamp reviewed_at when pilot mode auto-approves.

When newVerificationStatus is approved, the KYB row is approved but keeps reviewed_at null. Set it alongside the pilot auto-approval timestamp.

Suggested patch
   if (newVerificationStatus) {
     merchantPatch.verification_status = newVerificationStatus;
     merchantPatch.submitted_at = now;
+    if (newVerificationStatus === 'approved') {
+      merchantPatch.reviewed_at = now;
+    }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (newVerificationStatus) {
merchantPatch.verification_status = newVerificationStatus;
merchantPatch.submitted_at = now;
}
if (newVerificationStatus) {
merchantPatch.verification_status = newVerificationStatus;
merchantPatch.submitted_at = now;
if (newVerificationStatus === 'approved') {
merchantPatch.reviewed_at = now;
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relayer-service/server.js` around lines 1186 - 1189, The auto-approval path
in the verification update logic leaves reviewed_at unset when
newVerificationStatus becomes approved. Update the merchant patching flow around
the newVerificationStatus block to stamp reviewed_at at the same time as the
pilot auto-approval timestamp, alongside submitted_at, so the approval metadata
is complete.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Separate merchant contact verification from user login and add merchant KYB

1 participant