diff --git a/app/api/compliance/status/route.ts b/app/api/compliance/status/route.ts
new file mode 100644
index 0000000..9054ac5
--- /dev/null
+++ b/app/api/compliance/status/route.ts
@@ -0,0 +1,28 @@
+import { NextRequest, NextResponse } from "next/server";
+import { getCurrentUser } from "@/lib/server-auth";
+import { ComplianceService } from "@/lib/services/compliance";
+import { TermsService } from "@/lib/services/terms";
+
+export async function GET(request: NextRequest) {
+ try {
+ const user = await getCurrentUser();
+ if (!user) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ const compliance = await ComplianceService.getUserCompliance(user.id);
+ const remaining = await ComplianceService.getRemainingLimits(user.id);
+ const termsStatus = await TermsService.getUserTermsStatus(user.id);
+ const nextTier = ComplianceService.getNextTier(compliance.currentTier);
+
+ return NextResponse.json({
+ compliance,
+ remaining,
+ termsStatus,
+ nextTier,
+ });
+ } catch (error) {
+ console.error("Error fetching compliance status:", error);
+ return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
+ }
+}
diff --git a/app/api/compliance/terms/route.ts b/app/api/compliance/terms/route.ts
new file mode 100644
index 0000000..22ab0e5
--- /dev/null
+++ b/app/api/compliance/terms/route.ts
@@ -0,0 +1,35 @@
+import { NextRequest, NextResponse } from "next/server";
+import { getCurrentUser } from "@/lib/server-auth";
+import { TermsService } from "@/lib/services/terms";
+
+export async function GET(request: NextRequest) {
+ try {
+ const terms = await TermsService.getCurrentTermsVersion();
+ return NextResponse.json(terms);
+ } catch (error) {
+ return NextResponse.json({ error: "Failed to fetch terms" }, { status: 500 });
+ }
+}
+
+export async function POST(request: NextRequest) {
+ try {
+ const user = await getCurrentUser();
+ if (!user) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ const { termsVersionId } = await request.json();
+ const ip = request.headers.get('x-forwarded-for') || '0.0.0.0';
+ const userAgent = request.headers.get('user-agent') || 'unknown';
+
+ const acceptance = await TermsService.acceptTerms(user.id, termsVersionId, {
+ ipAddress: ip,
+ userAgent,
+ });
+
+ return NextResponse.json(acceptance);
+ } catch (error) {
+ console.error("Error accepting terms:", error);
+ return NextResponse.json({ error: "Failed to accept terms" }, { status: 500 });
+ }
+}
diff --git a/app/api/compliance/upgrade/route.ts b/app/api/compliance/upgrade/route.ts
new file mode 100644
index 0000000..c54ad2e
--- /dev/null
+++ b/app/api/compliance/upgrade/route.ts
@@ -0,0 +1,41 @@
+import { NextRequest, NextResponse } from "next/server";
+import { getCurrentUser } from "@/lib/server-auth";
+import { VerificationService } from "@/lib/services/verification";
+
+export async function POST(request: NextRequest) {
+ try {
+ const user = await getCurrentUser();
+ if (!user) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ const { targetTier } = await request.json();
+
+ const verificationRequest = await VerificationService.createVerificationRequest(
+ user.id,
+ targetTier
+ );
+
+ return NextResponse.json(verificationRequest);
+ } catch (error: any) {
+ console.error("Error creating verification request:", error);
+ return NextResponse.json(
+ { error: error.message || "Failed to create verification request" },
+ { status: 400 }
+ );
+ }
+}
+
+export async function GET(request: NextRequest) {
+ try {
+ const user = await getCurrentUser();
+ if (!user) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ const status = await VerificationService.getVerificationStatus(user.id);
+ return NextResponse.json(status);
+ } catch (error) {
+ return NextResponse.json({ error: "Failed to fetch verification status" }, { status: 500 });
+ }
+}
diff --git a/app/api/withdrawal/submit/route.ts b/app/api/withdrawal/submit/route.ts
new file mode 100644
index 0000000..ce43753
--- /dev/null
+++ b/app/api/withdrawal/submit/route.ts
@@ -0,0 +1,31 @@
+import { NextRequest, NextResponse } from "next/server";
+import { getCurrentUser } from "@/lib/server-auth";
+import { WithdrawalService } from "@/lib/services/withdrawal";
+
+export async function POST(request: NextRequest) {
+ try {
+ const user = await getCurrentUser();
+ if (!user) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ const { amount, currency, destinationId } = await request.json();
+ const ip = request.headers.get('x-forwarded-for') || '0.0.0.0';
+
+ const withdrawal = await WithdrawalService.submit(
+ user.id,
+ amount,
+ currency,
+ destinationId,
+ ip
+ );
+
+ return NextResponse.json(withdrawal);
+ } catch (error: any) {
+ console.error("Error submitting withdrawal:", error);
+ return NextResponse.json(
+ { error: error.message || "Withdrawal failed" },
+ { status: 400 }
+ );
+ }
+}
diff --git a/app/api/withdrawal/validate/route.ts b/app/api/withdrawal/validate/route.ts
new file mode 100644
index 0000000..ebabf0c
--- /dev/null
+++ b/app/api/withdrawal/validate/route.ts
@@ -0,0 +1,22 @@
+import { NextRequest, NextResponse } from "next/server";
+import { getCurrentUser } from "@/lib/server-auth";
+import { WithdrawalService } from "@/lib/services/withdrawal";
+
+export async function POST(request: NextRequest) {
+ try {
+ const user = await getCurrentUser();
+ if (!user) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ const { amount } = await request.json();
+ const ip = request.headers.get('x-forwarded-for') || '0.0.0.0';
+
+ const validation = await WithdrawalService.validate(user.id, amount, ip);
+
+ return NextResponse.json(validation);
+ } catch (error) {
+ console.error("Error validating withdrawal:", error);
+ return NextResponse.json({ error: "Validation failed" }, { status: 500 });
+ }
+}
diff --git a/app/wallet/page.tsx b/app/wallet/page.tsx
index 128d4b7..565d264 100644
--- a/app/wallet/page.tsx
+++ b/app/wallet/page.tsx
@@ -12,14 +12,14 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
export default function WalletPage() {
return (
-
+
Wallet
Manage your earnings, assets, and withdrawals.
-
+
diff --git a/components/compliance/appeal-dialog.tsx b/components/compliance/appeal-dialog.tsx
new file mode 100644
index 0000000..3cbbcd1
--- /dev/null
+++ b/components/compliance/appeal-dialog.tsx
@@ -0,0 +1,96 @@
+"use client";
+
+import { useState } from "react";
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { Label } from "@/components/ui/label";
+import { Textarea } from "@/components/ui/textarea";
+import { AppealService } from "@/lib/services/appeal";
+
+interface AppealDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ verificationRequestId: string;
+ userId: string;
+ rejectionReason?: string;
+}
+
+export function AppealDialog({ open, onOpenChange, verificationRequestId, userId, rejectionReason }: AppealDialogProps) {
+ const [reason, setReason] = useState("");
+ const [additionalInfo, setAdditionalInfo] = useState("");
+ const [submitting, setSubmitting] = useState(false);
+
+ const handleSubmit = async () => {
+ if (!reason.trim()) return;
+
+ setSubmitting(true);
+ try {
+ await AppealService.submitAppeal(userId, verificationRequestId, reason, additionalInfo);
+ alert('Appeal submitted successfully. Our team will review it within 3-5 business days.');
+ onOpenChange(false);
+ setReason("");
+ setAdditionalInfo("");
+ } catch (error: any) {
+ alert(error.message || 'Failed to submit appeal');
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/components/compliance/document-upload.tsx b/components/compliance/document-upload.tsx
new file mode 100644
index 0000000..bf932a7
--- /dev/null
+++ b/components/compliance/document-upload.tsx
@@ -0,0 +1,112 @@
+"use client";
+
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { Label } from "@/components/ui/label";
+import { Upload, X, FileText, CheckCircle } from "lucide-react";
+import { DocumentType } from "@/types/compliance";
+
+interface DocumentUploadProps {
+ type: DocumentType;
+ label: string;
+ onUpload: (file: File) => Promise
;
+ uploaded?: boolean;
+}
+
+const acceptedTypes: Record = {
+ GOVERNMENT_ID: "image/*,.pdf",
+ PROOF_OF_ADDRESS: "image/*,.pdf",
+ SELFIE: "image/*",
+ ADDITIONAL: "image/*,.pdf",
+};
+
+export function DocumentUpload({ type, label, onUpload, uploaded }: DocumentUploadProps) {
+ const [file, setFile] = useState(null);
+ const [uploading, setUploading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const handleFileChange = async (e: React.ChangeEvent) => {
+ const selectedFile = e.target.files?.[0];
+ if (!selectedFile) return;
+
+ if (selectedFile.size > 10 * 1024 * 1024) {
+ setError("File too large. Max 10MB.");
+ return;
+ }
+
+ setFile(selectedFile);
+ setError(null);
+
+ setUploading(true);
+ try {
+ await onUpload(selectedFile);
+ } catch (err: any) {
+ setError(err.message || "Upload failed");
+ } finally {
+ setUploading(false);
+ }
+ };
+
+ const clearFile = () => {
+ setFile(null);
+ setError(null);
+ };
+
+ return (
+
+
+
+ {!file && !uploaded && (
+
+ )}
+
+ {(file || uploaded) && (
+
+
+ {uploaded ? (
+
+ ) : (
+
+ )}
+
+
{file?.name || "Document uploaded"}
+
+ {uploaded ? "Verified" : uploading ? "Uploading..." : "Ready"}
+
+
+
+ {!uploaded && !uploading && (
+
+ )}
+
+ )}
+
+ {error && (
+
{error}
+ )}
+
+ );
+}
diff --git a/components/compliance/hold-message.tsx b/components/compliance/hold-message.tsx
new file mode 100644
index 0000000..eb1a367
--- /dev/null
+++ b/components/compliance/hold-message.tsx
@@ -0,0 +1,48 @@
+"use client";
+
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
+import { AlertCircle, Ban, Shield } from "lucide-react";
+import { ComplianceHoldState } from "@/types/compliance";
+
+interface HoldMessageProps {
+ holdState: ComplianceHoldState;
+ reason?: string;
+}
+
+export function HoldMessage({ holdState, reason }: HoldMessageProps) {
+ if (holdState === 'NONE') return null;
+
+ const config = {
+ REVIEW: {
+ icon: Shield,
+ variant: 'default' as const,
+ title: 'Account Under Review',
+ description: 'Your account is currently under review. Withdrawals are temporarily paused.',
+ },
+ SUSPENDED: {
+ icon: AlertCircle,
+ variant: 'destructive' as const,
+ title: 'Account Suspended',
+ description: 'Your account has been suspended. Please contact support for more information.',
+ },
+ BLOCKED: {
+ icon: Ban,
+ variant: 'destructive' as const,
+ title: 'Account Blocked',
+ description: 'Withdrawals are blocked on your account. Please contact support immediately.',
+ },
+ };
+
+ const { icon: Icon, variant, title, description } = config[holdState];
+
+ return (
+
+
+ {title}
+
+ {description}
+ {reason && Reason: {reason}
}
+
+
+ );
+}
diff --git a/components/compliance/limits-display.tsx b/components/compliance/limits-display.tsx
new file mode 100644
index 0000000..d93fc64
--- /dev/null
+++ b/components/compliance/limits-display.tsx
@@ -0,0 +1,101 @@
+"use client";
+
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { ArrowUp } from "lucide-react";
+import { KYCTier } from "@/types/compliance";
+import { useComplianceStatus } from "@/hooks/use-compliance";
+
+interface LimitsDisplayProps {
+ onUpgradeClick?: () => void;
+}
+
+const tierColors: Record = {
+ UNVERIFIED: 'bg-gray-500',
+ BASIC: 'bg-blue-500',
+ VERIFIED: 'bg-green-500',
+ ENHANCED: 'bg-purple-500',
+};
+
+const formatCurrency = (amount: number) =>
+ new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(amount);
+
+export function LimitsDisplay({ onUpgradeClick }: LimitsDisplayProps) {
+ const { data, isLoading } = useComplianceStatus();
+
+ if (isLoading) {
+ return Loading limits...
;
+ }
+
+ if (!data) return null;
+
+ const { compliance, remaining, nextTier } = data;
+
+ return (
+
+
+
+ Verification Tier:
+
+ {compliance.currentTier}
+
+
+ {nextTier && onUpgradeClick && (
+
+ )}
+
+
+
+
+
+
+
+
+ );
+}
+
+function LimitBar({ label, used, total, remaining, percent }: {
+ label: string;
+ used: number;
+ total: number;
+ remaining: number;
+ percent: number;
+}) {
+ return (
+
+
+ {label}
+
+ {formatCurrency(remaining)} / {formatCurrency(total)}
+
+
+
+
+ );
+}
diff --git a/components/compliance/terms-dialog.tsx b/components/compliance/terms-dialog.tsx
new file mode 100644
index 0000000..f8823d7
--- /dev/null
+++ b/components/compliance/terms-dialog.tsx
@@ -0,0 +1,88 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { Checkbox } from "@/components/ui/checkbox";
+import { useQuery } from "@tanstack/react-query";
+import { get } from "@/lib/api/client";
+import { TermsVersion } from "@/types/terms";
+import { useAcceptTerms } from "@/hooks/use-compliance";
+
+interface TermsDialogProps {
+ open: boolean;
+ onOpenChange?: (open: boolean) => void;
+ onAccepted: () => void;
+}
+
+export function TermsDialog({ open, onOpenChange, onAccepted }: TermsDialogProps) {
+ const [agreed, setAgreed] = useState(false);
+ const [scrolled, setScrolled] = useState(false);
+
+ const { data: terms } = useQuery({
+ queryKey: ['terms'],
+ queryFn: () => get('/api/compliance/terms'),
+ });
+
+ const acceptMutation = useAcceptTerms();
+
+ const handleAccept = async () => {
+ if (!terms || !agreed) return;
+
+ try {
+ await acceptMutation.mutateAsync(terms.id);
+ onAccepted();
+ } catch (error) {
+ console.error('Failed to accept terms:', error);
+ }
+ };
+
+ const handleScroll = (e: React.UIEvent) => {
+ const target = e.target as HTMLDivElement;
+ const bottom = target.scrollHeight - target.scrollTop <= target.clientHeight + 50;
+ if (bottom) setScrolled(true);
+ };
+
+ return (
+
+ );
+}
diff --git a/components/compliance/tier-upgrade-dialog.tsx b/components/compliance/tier-upgrade-dialog.tsx
new file mode 100644
index 0000000..3800a29
--- /dev/null
+++ b/components/compliance/tier-upgrade-dialog.tsx
@@ -0,0 +1,160 @@
+"use client";
+
+import { useState } from "react";
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { Check } from "lucide-react";
+import { KYCTier, DocumentType } from "@/types/compliance";
+import { ComplianceService } from "@/lib/services/compliance";
+import { VerificationService } from "@/lib/services/verification";
+import { useUpgradeTier } from "@/hooks/use-compliance";
+import { DocumentUpload } from "./document-upload";
+
+interface TierUpgradeDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ currentTier: KYCTier;
+ targetTier: KYCTier;
+}
+
+const formatCurrency = (amount: number) =>
+ new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0 }).format(amount);
+
+export function TierUpgradeDialog({ open, onOpenChange, currentTier, targetTier }: TierUpgradeDialogProps) {
+ const [step, setStep] = useState<'info' | 'documents'>('info');
+ const [requestId, setRequestId] = useState(null);
+ const [uploadedDocs, setUploadedDocs] = useState>(new Set());
+ const upgradeMutation = useUpgradeTier();
+ const tierConfig = ComplianceService.getTierConfig(targetTier);
+ const requiredDocs = VerificationService.getRequiredDocuments(targetTier);
+
+ const handleUpgrade = async () => {
+ try {
+ const request = await upgradeMutation.mutateAsync(targetTier);
+ setRequestId(request.id);
+ if (requiredDocs.length > 0) {
+ setStep('documents');
+ } else {
+ onOpenChange(false);
+ }
+ } catch (error: any) {
+ alert(error.message || 'Failed to request upgrade');
+ }
+ };
+
+ const handleDocumentUpload = async (type: DocumentType, file: File) => {
+ if (!requestId) return;
+
+ await VerificationService.uploadDocument(requestId, {
+ type,
+ fileName: file.name,
+ file,
+ });
+
+ setUploadedDocs(prev => new Set(prev).add(type));
+ };
+
+ const handleComplete = () => {
+ setStep('info');
+ setUploadedDocs(new Set());
+ setRequestId(null);
+ onOpenChange(false);
+ };
+
+ return (
+
+ );
+}
diff --git a/components/ui/checkbox.tsx b/components/ui/checkbox.tsx
index cb0b07b..05cf97e 100644
--- a/components/ui/checkbox.tsx
+++ b/components/ui/checkbox.tsx
@@ -14,7 +14,7 @@ function Checkbox({
(null);
+
+ const { data: complianceData } = useComplianceStatus();
+ const validateMutation = useValidateWithdrawal();
+ const submitMutation = useSubmitWithdrawal();
- // Mock bank accounts
const bankAccounts = [
{ id: '1', name: 'Chase Bank', last4: '4242', isPrimary: true },
];
const parsedAmount = parseFloat(amount);
- const isValidAmount = !isNaN(parsedAmount) &&
- isFinite(parsedAmount) &&
- parsedAmount >= 10 &&
- parsedAmount <= walletInfo.balance;
+ const isValidAmount = !isNaN(parsedAmount) && isFinite(parsedAmount) && parsedAmount >= 10;
+
+ useEffect(() => {
+ if (isValidAmount && parsedAmount <= walletInfo.balance) {
+ validateMutation.mutate(parsedAmount, {
+ onSuccess: (result) => {
+ if (!result.valid) {
+ setValidationError(result.errors[0] || 'Validation failed');
+ } else {
+ setValidationError(null);
+ }
+ },
+ });
+ } else {
+ setValidationError(null);
+ }
+ }, [parsedAmount]);
+
+ const handleWithdraw = async () => {
+ if (!isValidAmount || !complianceData) return;
+
+ try {
+ await submitMutation.mutateAsync({
+ amount: parsedAmount,
+ currency: 'USD',
+ destinationId: bankAccounts[0].id,
+ });
+ alert('Withdrawal submitted successfully!');
+ setAmount('');
+ } catch (error: any) {
+ alert(error.message || 'Withdrawal failed');
+ }
+ };
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat("en-US", {
@@ -34,8 +76,40 @@ export function WithdrawalSection({ walletInfo }: WithdrawalSectionProps) {
}).format(amount);
};
+ const canWithdraw = isValidAmount &&
+ parsedAmount <= walletInfo.balance &&
+ !validationError &&
+ complianceData?.compliance.holdState === 'NONE' &&
+ !complianceData?.termsStatus.requiresAcceptance;
+
return (
+ {complianceData && (
+ <>
+
+
+
{
+ setShowTermsDialog(false);
+ }}
+ />
+
+ {complianceData.nextTier && (
+
+ )}
+ >
+ )}
+
Off-Ramp to Fiat
@@ -68,6 +142,12 @@ export function WithdrawalSection({ walletInfo }: WithdrawalSectionProps) {
Balance: {formatCurrency(walletInfo.balance)}
Min: $10.00
+ {validationError && (
+
+
+ {validationError}
+
+ )}
@@ -110,16 +190,22 @@ export function WithdrawalSection({ walletInfo }: WithdrawalSectionProps) {
+ {complianceData?.termsStatus.requiresAcceptance && (
+
+ )}
+
@@ -148,22 +234,7 @@ export function WithdrawalSection({ walletInfo }: WithdrawalSectionProps) {
Limits & Settings
-
-
- Daily Limit
- $5,000 / $5,000
-
-
-
- Monthly Limit
- $50,000 / $50,000
-
-
-
+
setShowUpgradeDialog(true)} />
diff --git a/hooks/use-compliance.ts b/hooks/use-compliance.ts
new file mode 100644
index 0000000..7163ee9
--- /dev/null
+++ b/hooks/use-compliance.ts
@@ -0,0 +1,42 @@
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { get, post } from '@/lib/api/client';
+import { UserCompliance, RemainingLimits, KYCTier, VerificationRequest } from '@/types/compliance';
+import { UserTermsStatus } from '@/types/terms';
+
+interface ComplianceStatus {
+ compliance: UserCompliance;
+ remaining: RemainingLimits;
+ termsStatus: UserTermsStatus;
+ nextTier: KYCTier | null;
+}
+
+export function useComplianceStatus() {
+ return useQuery({
+ queryKey: ['compliance', 'status'],
+ queryFn: () => get('/api/compliance/status'),
+ });
+}
+
+export function useAcceptTerms() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (termsVersionId: string) =>
+ post('/api/compliance/terms', { termsVersionId }),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['compliance'] });
+ },
+ });
+}
+
+export function useUpgradeTier() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (targetTier: KYCTier) =>
+ post('/api/compliance/upgrade', { targetTier }),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['compliance'] });
+ },
+ });
+}
diff --git a/hooks/use-withdrawal.ts b/hooks/use-withdrawal.ts
new file mode 100644
index 0000000..970694c
--- /dev/null
+++ b/hooks/use-withdrawal.ts
@@ -0,0 +1,22 @@
+import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { post } from '@/lib/api/client';
+import { WithdrawalValidationResult, WithdrawalRequest } from '@/types/withdrawal';
+
+export function useValidateWithdrawal() {
+ return useMutation({
+ mutationFn: (amount: number) =>
+ post('/api/withdrawal/validate', { amount }),
+ });
+}
+
+export function useSubmitWithdrawal() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (data: { amount: number; currency: string; destinationId: string }) =>
+ post('/api/withdrawal/submit', data),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['compliance'] });
+ },
+ });
+}
diff --git a/lib/query/query-keys.ts b/lib/query/query-keys.ts
index d4956fd..6343804 100644
--- a/lib/query/query-keys.ts
+++ b/lib/query/query-keys.ts
@@ -36,3 +36,18 @@ export const authKeys = {
all: ['auth'] as const,
session: () => [...authKeys.all, 'session'] as const,
};
+
+export const complianceKeys = {
+ all: ['compliance'] as const,
+ status: () => [...complianceKeys.all, 'status'] as const,
+};
+
+export const termsKeys = {
+ all: ['terms'] as const,
+ current: () => [...termsKeys.all, 'current'] as const,
+};
+
+export const withdrawalKeys = {
+ all: ['withdrawal'] as const,
+ history: () => [...withdrawalKeys.all, 'history'] as const,
+};
diff --git a/lib/services/appeal.ts b/lib/services/appeal.ts
new file mode 100644
index 0000000..272bd49
--- /dev/null
+++ b/lib/services/appeal.ts
@@ -0,0 +1,44 @@
+import { VerificationAppeal, AppealStatus } from "@/types/compliance";
+import { EmailService } from "./email";
+
+const MOCK_APPEALS: Record = {};
+
+export class AppealService {
+ static async submitAppeal(
+ userId: string,
+ verificationRequestId: string,
+ reason: string,
+ additionalInfo?: string
+ ): Promise {
+ const appealId = `appeal-${userId}-${Date.now()}`;
+ const appeal: VerificationAppeal = {
+ id: appealId,
+ userId,
+ verificationRequestId,
+ reason,
+ additionalInfo,
+ status: 'PENDING',
+ submittedAt: new Date().toISOString(),
+ };
+
+ MOCK_APPEALS[appealId] = appeal;
+
+ const userEmail = `user-${userId}@example.com`;
+ await EmailService.sendAppealConfirmation(userEmail, appealId);
+
+ return { ...appeal };
+ }
+
+ static async getAppeal(verificationRequestId: string): Promise {
+ const appeal = Object.values(MOCK_APPEALS)
+ .find(a => a.verificationRequestId === verificationRequestId);
+
+ return appeal ? { ...appeal } : null;
+ }
+
+ static async getUserAppeals(userId: string): Promise {
+ return Object.values(MOCK_APPEALS)
+ .filter(a => a.userId === userId)
+ .sort((a, b) => new Date(b.submittedAt).getTime() - new Date(a.submittedAt).getTime());
+ }
+}
diff --git a/lib/services/compliance.ts b/lib/services/compliance.ts
new file mode 100644
index 0000000..07437bd
--- /dev/null
+++ b/lib/services/compliance.ts
@@ -0,0 +1,350 @@
+import {
+ UserCompliance,
+ KYCTier,
+ KYCTierConfig,
+ WithdrawalLimits,
+ WithdrawalUsage,
+ RemainingLimits,
+ ComplianceHoldState,
+ VerificationStatus,
+} from "@/types/compliance";
+
+// Mock Data Store (In-memory for prototype)
+const MOCK_COMPLIANCE_DB: Record = {};
+
+export class ComplianceService {
+ // KYC Tier Configuration
+ static readonly TIER_CONFIGS: Record = {
+ UNVERIFIED: {
+ tier: 'UNVERIFIED',
+ description: 'Basic account access with limited withdrawal capabilities',
+ limits: {
+ daily: 100,
+ weekly: 300,
+ monthly: 500,
+ perTransaction: 50,
+ },
+ requirements: [
+ 'Email verification required',
+ 'No additional documentation needed',
+ ],
+ processingTime: 'Instant',
+ },
+ BASIC: {
+ tier: 'BASIC',
+ description: 'Standard account with moderate withdrawal limits',
+ limits: {
+ daily: 1000,
+ weekly: 5000,
+ monthly: 15000,
+ perTransaction: 500,
+ },
+ requirements: [
+ 'Email verification',
+ 'Phone number verification',
+ 'Basic personal information (name, date of birth)',
+ ],
+ processingTime: '1-2 business days',
+ },
+ VERIFIED: {
+ tier: 'VERIFIED',
+ description: 'Verified account with higher withdrawal limits',
+ limits: {
+ daily: 5000,
+ weekly: 25000,
+ monthly: 75000,
+ perTransaction: 2500,
+ },
+ requirements: [
+ 'All BASIC requirements',
+ 'Government-issued ID (passport, driver\'s license)',
+ 'Proof of address (utility bill, bank statement)',
+ ],
+ processingTime: '2-5 business days',
+ },
+ ENHANCED: {
+ tier: 'ENHANCED',
+ description: 'Premium account with maximum withdrawal limits',
+ limits: {
+ daily: 25000,
+ weekly: 100000,
+ monthly: 300000,
+ perTransaction: 10000,
+ },
+ requirements: [
+ 'All VERIFIED requirements',
+ 'Enhanced identity verification',
+ 'Video verification call',
+ 'Source of funds documentation',
+ ],
+ processingTime: '5-10 business days',
+ },
+ };
+
+ /**
+ * Get user's current compliance status
+ */
+ static async getUserCompliance(userId: string): Promise {
+ await this.simulateDelay();
+
+ // Return existing or create new
+ if (!MOCK_COMPLIANCE_DB[userId]) {
+ MOCK_COMPLIANCE_DB[userId] = this.createDefaultCompliance(userId);
+ }
+
+ return { ...MOCK_COMPLIANCE_DB[userId] };
+ }
+
+ /**
+ * Create default compliance record for new user
+ */
+ private static createDefaultCompliance(userId: string): UserCompliance {
+ const now = new Date().toISOString();
+ const tier: KYCTier = 'UNVERIFIED';
+
+ return {
+ userId,
+ currentTier: tier,
+ limits: { ...this.TIER_CONFIGS[tier].limits },
+ usage: {
+ dailyUsed: 0,
+ weeklyUsed: 0,
+ monthlyUsed: 0,
+ lifetimeTotal: 0,
+ lastResetDaily: now,
+ lastResetWeekly: now,
+ lastResetMonthly: now,
+ },
+ holdState: 'NONE',
+ verificationStatus: 'NOT_STARTED',
+ restrictedJurisdiction: false,
+ createdAt: now,
+ updatedAt: now,
+ };
+ }
+
+ /**
+ * Calculate remaining withdrawal amounts with rolling windows
+ */
+ static async getRemainingLimits(userId: string): Promise {
+ const compliance = await this.getUserCompliance(userId);
+
+ // Reset usage if windows have passed
+ await this.resetExpiredWindows(userId, compliance);
+
+ const { limits, usage } = compliance;
+
+ const dailyRemaining = Math.max(0, limits.daily - usage.dailyUsed);
+ const weeklyRemaining = Math.max(0, limits.weekly - usage.weeklyUsed);
+ const monthlyRemaining = Math.max(0, limits.monthly - usage.monthlyUsed);
+
+ return {
+ daily: dailyRemaining,
+ weekly: weeklyRemaining,
+ monthly: monthlyRemaining,
+ percentUsed: {
+ daily: limits.daily > 0 ? Math.min(100, (usage.dailyUsed / limits.daily) * 100) : 0,
+ weekly: limits.weekly > 0 ? Math.min(100, (usage.weeklyUsed / limits.weekly) * 100) : 0,
+ monthly: limits.monthly > 0 ? Math.min(100, (usage.monthlyUsed / limits.monthly) * 100) : 0,
+ },
+ };
+ }
+
+ /**
+ * Reset usage for expired rolling windows
+ */
+ private static async resetExpiredWindows(
+ userId: string,
+ compliance: UserCompliance
+ ): Promise {
+ const now = new Date();
+ const { usage } = compliance;
+ let updated = false;
+
+ // Daily reset (24 hours)
+ const lastDaily = new Date(usage.lastResetDaily);
+ if (now.getTime() - lastDaily.getTime() >= 24 * 60 * 60 * 1000) {
+ compliance.usage.dailyUsed = 0;
+ compliance.usage.lastResetDaily = now.toISOString();
+ updated = true;
+ }
+
+ // Weekly reset (7 days)
+ const lastWeekly = new Date(usage.lastResetWeekly);
+ if (now.getTime() - lastWeekly.getTime() >= 7 * 24 * 60 * 60 * 1000) {
+ compliance.usage.weeklyUsed = 0;
+ compliance.usage.lastResetWeekly = now.toISOString();
+ updated = true;
+ }
+
+ // Monthly reset (30 days)
+ const lastMonthly = new Date(usage.lastResetMonthly);
+ if (now.getTime() - lastMonthly.getTime() >= 30 * 24 * 60 * 60 * 1000) {
+ compliance.usage.monthlyUsed = 0;
+ compliance.usage.lastResetMonthly = now.toISOString();
+ updated = true;
+ }
+
+ if (updated) {
+ compliance.updatedAt = now.toISOString();
+ MOCK_COMPLIANCE_DB[userId] = compliance;
+ }
+ }
+
+ /**
+ * Check if withdrawal would exceed any limits
+ */
+ static async validateWithdrawalAmount(
+ userId: string,
+ amount: number
+ ): Promise<{ valid: boolean; exceededLimit?: 'daily' | 'weekly' | 'monthly' | 'perTransaction' }> {
+ const compliance = await this.getUserCompliance(userId);
+ await this.resetExpiredWindows(userId, compliance);
+
+ const { limits, usage } = compliance;
+
+ // Check per-transaction limit
+ if (amount > limits.perTransaction) {
+ return { valid: false, exceededLimit: 'perTransaction' };
+ }
+
+ // Check daily limit
+ if (usage.dailyUsed + amount > limits.daily) {
+ return { valid: false, exceededLimit: 'daily' };
+ }
+
+ // Check weekly limit
+ if (usage.weeklyUsed + amount > limits.weekly) {
+ return { valid: false, exceededLimit: 'weekly' };
+ }
+
+ // Check monthly limit
+ if (usage.monthlyUsed + amount > limits.monthly) {
+ return { valid: false, exceededLimit: 'monthly' };
+ }
+
+ return { valid: true };
+ }
+
+ /**
+ * Track withdrawal usage (call after successful withdrawal)
+ */
+ static async trackWithdrawal(userId: string, amount: number): Promise {
+ const compliance = await this.getUserCompliance(userId);
+
+ compliance.usage.dailyUsed += amount;
+ compliance.usage.weeklyUsed += amount;
+ compliance.usage.monthlyUsed += amount;
+ compliance.usage.lifetimeTotal += amount;
+ compliance.updatedAt = new Date().toISOString();
+
+ MOCK_COMPLIANCE_DB[userId] = compliance;
+ }
+
+ /**
+ * Upgrade user tier after verification approval
+ */
+ static async upgradeTier(userId: string, newTier: KYCTier): Promise {
+ const compliance = await this.getUserCompliance(userId);
+
+ // Update tier and limits
+ compliance.currentTier = newTier;
+ compliance.limits = { ...this.TIER_CONFIGS[newTier].limits };
+ compliance.verificationStatus = 'APPROVED';
+ compliance.updatedAt = new Date().toISOString();
+
+ MOCK_COMPLIANCE_DB[userId] = compliance;
+ return true;
+ }
+
+ /**
+ * Set hold state on user account
+ */
+ static async setHoldState(
+ userId: string,
+ state: ComplianceHoldState,
+ reason?: string
+ ): Promise {
+ const compliance = await this.getUserCompliance(userId);
+
+ compliance.holdState = state;
+ compliance.holdReason = reason;
+ compliance.updatedAt = new Date().toISOString();
+
+ MOCK_COMPLIANCE_DB[userId] = compliance;
+ }
+
+ /**
+ * Update verification status
+ */
+ static async updateVerificationStatus(
+ userId: string,
+ status: VerificationStatus
+ ): Promise {
+ const compliance = await this.getUserCompliance(userId);
+
+ compliance.verificationStatus = status;
+ compliance.updatedAt = new Date().toISOString();
+
+ MOCK_COMPLIANCE_DB[userId] = compliance;
+ }
+
+ /**
+ * Mark user as restricted jurisdiction
+ */
+ static async setJurisdictionRestriction(
+ userId: string,
+ restricted: boolean,
+ jurisdictionCode?: string
+ ): Promise {
+ const compliance = await this.getUserCompliance(userId);
+
+ compliance.restrictedJurisdiction = restricted;
+ compliance.jurisdictionCode = jurisdictionCode;
+ compliance.updatedAt = new Date().toISOString();
+
+ MOCK_COMPLIANCE_DB[userId] = compliance;
+ }
+
+ /**
+ * Get tier configuration
+ */
+ static getTierConfig(tier: KYCTier): KYCTierConfig {
+ return { ...this.TIER_CONFIGS[tier] };
+ }
+
+ /**
+ * Get all tier configurations
+ */
+ static getAllTierConfigs(): KYCTierConfig[] {
+ return Object.values(this.TIER_CONFIGS);
+ }
+
+ /**
+ * Get next tier
+ */
+ static getNextTier(currentTier: KYCTier): KYCTier | null {
+ const tiers: KYCTier[] = ['UNVERIFIED', 'BASIC', 'VERIFIED', 'ENHANCED'];
+ const currentIndex = tiers.indexOf(currentTier);
+
+ if (currentIndex === -1 || currentIndex === tiers.length - 1) {
+ return null;
+ }
+
+ return tiers[currentIndex + 1];
+ }
+
+ /**
+ * Simulate database delay
+ */
+ private static async simulateDelay(): Promise {
+ await new Promise(resolve => setTimeout(resolve, 100));
+ }
+
+ /**
+ * Clear all compliance data (for testing)
+ */
+ static clearAllData(): void {
+ Object.keys(MOCK_COMPLIANCE_DB).forEach(key => delete MOCK_COMPLIANCE_DB[key]);
+ }
+}
diff --git a/lib/services/email.ts b/lib/services/email.ts
new file mode 100644
index 0000000..3ad26a7
--- /dev/null
+++ b/lib/services/email.ts
@@ -0,0 +1,145 @@
+import { VerificationStatus } from "@/types/compliance";
+
+interface EmailData {
+ to: string;
+ subject: string;
+ body: string;
+}
+
+export class EmailService {
+ static async sendVerificationStatusEmail(
+ userEmail: string,
+ status: VerificationStatus,
+ tier?: string,
+ reason?: string
+ ): Promise {
+ const emailData = this.buildVerificationEmail(userEmail, status, tier, reason);
+ await this.send(emailData);
+ }
+
+ static async sendWithdrawalConfirmation(
+ userEmail: string,
+ amount: number,
+ currency: string,
+ withdrawalId: string
+ ): Promise {
+ const emailData: EmailData = {
+ to: userEmail,
+ subject: "Withdrawal Confirmation",
+ body: `
+ Withdrawal Submitted
+ Your withdrawal of ${amount} ${currency} has been submitted successfully.
+ Withdrawal ID: ${withdrawalId}
+ Processing typically takes 1-5 business days.
+ `,
+ };
+ await this.send(emailData);
+ }
+
+ static async sendTermsUpdateNotification(
+ userEmail: string,
+ version: string
+ ): Promise {
+ const emailData: EmailData = {
+ to: userEmail,
+ subject: "Terms and Conditions Updated",
+ body: `
+ Terms and Conditions Update
+ Our Terms and Conditions have been updated to version ${version}.
+ Please review and accept the new terms before your next withdrawal.
+ Review Terms
+ `,
+ };
+ await this.send(emailData);
+ }
+
+ static async sendAppealConfirmation(
+ userEmail: string,
+ appealId: string
+ ): Promise {
+ const emailData: EmailData = {
+ to: userEmail,
+ subject: "Verification Appeal Received",
+ body: `
+ Appeal Received
+ Your verification appeal has been received and is under review.
+ Appeal ID: ${appealId}
+ Our team will review your appeal within 3-5 business days.
+ `,
+ };
+ await this.send(emailData);
+ }
+
+ private static buildVerificationEmail(
+ userEmail: string,
+ status: VerificationStatus,
+ tier?: string,
+ reason?: string
+ ): EmailData {
+ const templates: Record = {
+ PENDING: {
+ subject: "Verification Request Received",
+ body: `
+ Verification Request Received
+ We've received your request to upgrade to ${tier} tier.
+ Our team will review your documents within 2-5 business days.
+ You'll receive an email once the review is complete.
+ `,
+ },
+ APPROVED: {
+ subject: "Verification Approved!",
+ body: `
+ Congratulations!
+ Your verification for ${tier} tier has been approved.
+ Your new withdrawal limits are now active.
+ View Your Limits
+ `,
+ },
+ REJECTED: {
+ subject: "Verification Update Required",
+ body: `
+ Additional Information Needed
+ We couldn't complete your verification for ${tier} tier.
+ ${reason ? `Reason: ${reason}
` : ''}
+ You can submit an appeal or resubmit your documents.
+ Take Action
+ `,
+ },
+ NOT_STARTED: {
+ subject: "Start Your Verification",
+ body: `
+ Increase Your Withdrawal Limits
+ Complete verification to increase your withdrawal limits.
+ Get Started
+ `,
+ },
+ };
+
+ const template = templates[status];
+ return {
+ to: userEmail,
+ subject: template.subject,
+ body: template.body,
+ };
+ }
+
+ private static async send(emailData: EmailData): Promise {
+ // In production, integrate with email provider:
+ // - SendGrid: await sgMail.send(emailData)
+ // - AWS SES: await ses.sendEmail(emailData)
+ // - Resend: await resend.emails.send(emailData)
+
+ console.log('[Email Service] Sending email:', {
+ to: emailData.to,
+ subject: emailData.subject,
+ });
+
+ // Mock delay
+ await new Promise(resolve => setTimeout(resolve, 100));
+
+ // For development, log the email
+ if (process.env.NODE_ENV === 'development') {
+ console.log('[Email Preview]:', emailData.body);
+ }
+ }
+}
diff --git a/lib/services/geo-restriction.ts b/lib/services/geo-restriction.ts
new file mode 100644
index 0000000..19ecd01
--- /dev/null
+++ b/lib/services/geo-restriction.ts
@@ -0,0 +1,104 @@
+import { RestrictedJurisdiction, UserLocation } from "@/types/geography";
+
+const RESTRICTED: RestrictedJurisdiction[] = [
+ { code: 'CU', name: 'Cuba', type: 'COUNTRY', reason: 'OFAC sanctions', effectiveDate: '2024-01-01' },
+ { code: 'IR', name: 'Iran', type: 'COUNTRY', reason: 'OFAC sanctions', effectiveDate: '2024-01-01' },
+ { code: 'KP', name: 'North Korea', type: 'COUNTRY', reason: 'OFAC sanctions', effectiveDate: '2024-01-01' },
+ { code: 'SY', name: 'Syria', type: 'COUNTRY', reason: 'OFAC sanctions', effectiveDate: '2024-01-01' },
+ { code: 'US-NY', name: 'New York', type: 'STATE', reason: 'BitLicense requirements', effectiveDate: '2024-01-01' },
+];
+
+const VPN_INDICATORS = [
+ 'vpn', 'proxy', 'tor', 'relay', 'hosting', 'datacenter'
+];
+
+export class GeoRestrictionService {
+ static async checkLocation(ip: string): Promise {
+ // In production: call ipapi.co, MaxMind, or ip-api.com
+ // Example: const response = await fetch(`https://ipapi.co/${ip}/json/`)
+
+ const mockLocation: UserLocation = {
+ ip,
+ countryCode: 'US',
+ countryName: 'United States',
+ regionCode: 'CA',
+ regionName: 'California',
+ city: 'San Francisco',
+ isVPN: await this.detectVPN(ip),
+ isProxy: await this.detectProxy(ip),
+ isRestricted: false,
+ };
+
+ // Check country-level restrictions
+ const countryRestriction = RESTRICTED.find(r => r.code === mockLocation.countryCode && r.type === 'COUNTRY');
+ if (countryRestriction) {
+ mockLocation.isRestricted = true;
+ mockLocation.restrictionReason = countryRestriction.reason;
+ }
+
+ // Check state-level restrictions
+ if (mockLocation.countryCode === 'US' && mockLocation.regionCode) {
+ const stateCode = `US-${mockLocation.regionCode}`;
+ const stateRestriction = RESTRICTED.find(r => r.code === stateCode && r.type === 'STATE');
+ if (stateRestriction) {
+ mockLocation.isRestricted = true;
+ mockLocation.restrictionReason = stateRestriction.reason;
+ }
+ }
+
+ return mockLocation;
+ }
+
+ static async detectVPN(ip: string): Promise {
+ // In production: use VPN detection API like:
+ // - IPHub: https://iphub.info/
+ // - IP2Proxy: https://www.ip2location.com/
+ // - VPNApi: https://vpnapi.io/
+
+ // Mock: check if IP is in common VPN ranges
+ const vpnRanges = ['10.', '172.16.', '192.168.'];
+ return vpnRanges.some(range => ip.startsWith(range));
+ }
+
+ static async detectProxy(ip: string): Promise {
+ // In production: check proxy databases or use APIs
+ // For now, use similar logic as VPN detection
+ return false;
+ }
+
+ static isRestricted(countryCode: string, regionCode?: string): boolean {
+ // Check country restriction
+ if (RESTRICTED.some(r => r.code === countryCode && r.type === 'COUNTRY')) {
+ return true;
+ }
+
+ // Check state restriction
+ if (regionCode) {
+ const stateCode = `${countryCode}-${regionCode}`;
+ return RESTRICTED.some(r => r.code === stateCode && r.type === 'STATE');
+ }
+
+ return false;
+ }
+
+ static getRestrictionReason(countryCode: string, regionCode?: string): string | null {
+ const countryRestriction = RESTRICTED.find(r => r.code === countryCode && r.type === 'COUNTRY');
+ if (countryRestriction) {
+ return countryRestriction.reason;
+ }
+
+ if (regionCode) {
+ const stateCode = `${countryCode}-${regionCode}`;
+ const stateRestriction = RESTRICTED.find(r => r.code === stateCode && r.type === 'STATE');
+ if (stateRestriction) {
+ return stateRestriction.reason;
+ }
+ }
+
+ return null;
+ }
+
+ static getRestrictedJurisdictions(): RestrictedJurisdiction[] {
+ return [...RESTRICTED];
+ }
+}
diff --git a/lib/services/terms.ts b/lib/services/terms.ts
new file mode 100644
index 0000000..4e69027
--- /dev/null
+++ b/lib/services/terms.ts
@@ -0,0 +1,255 @@
+import {
+ TermsVersion,
+ TermsAcceptance,
+ UserTermsStatus,
+} from "@/types/terms";
+
+// Mock Data Stores
+const MOCK_TERMS_VERSIONS: TermsVersion[] = [
+ {
+ id: 'terms-v1',
+ version: '1.0.0',
+ title: 'Withdrawal Terms and Conditions',
+ summary: 'Standard terms for fiat withdrawal services',
+ content: `# Withdrawal Terms and Conditions
+
+## 1. General Terms
+
+By using Boundless Bounties withdrawal services, you agree to comply with these terms and all applicable laws and regulations.
+
+## 2. Withdrawal Limits
+
+Withdrawal limits are determined by your KYC verification tier:
+- Unverified: $100 daily, $500 monthly
+- Basic: $1,000 daily, $15,000 monthly
+- Verified: $5,000 daily, $75,000 monthly
+- Enhanced: $25,000 daily, $300,000 monthly
+
+## 3. Processing Times
+
+- Withdrawals are processed within 1-5 business days
+- Additional verification may be required for large transactions
+- Weekends and holidays may extend processing times
+
+## 4. Fees
+
+- Standard withdrawal fee: $2.50 per transaction
+- Additional fees may apply for expedited processing
+- Currency conversion fees apply for non-USD withdrawals
+
+## 5. Compliance Requirements
+
+- You must maintain accurate personal information
+- Additional verification may be requested at any time
+- We reserve the right to freeze or cancel transactions for compliance reasons
+
+## 6. Geographic Restrictions
+
+- Services may not be available in all jurisdictions
+- You must not use VPN or proxy services to bypass restrictions
+- We comply with all local financial regulations
+
+## 7. Account Security
+
+- You are responsible for maintaining account security
+- Report any unauthorized access immediately
+- Two-factor authentication is strongly recommended
+
+## 8. Dispute Resolution
+
+- Disputes must be reported within 30 days
+- We will investigate all disputes thoroughly
+- Resolution may take 15-30 business days
+
+## 9. Changes to Terms
+
+- We reserve the right to update these terms
+- You will be notified of material changes
+- Continued use constitutes acceptance of new terms
+
+## 10. Contact
+
+For questions or concerns, contact support@boundless.fi`,
+ effectiveDate: '2024-01-01T00:00:00Z',
+ requiresReacceptance: false,
+ createdAt: '2024-01-01T00:00:00Z',
+ },
+];
+
+const MOCK_TERMS_ACCEPTANCES: Record = {};
+
+export class TermsService {
+ /**
+ * Get current active terms version
+ */
+ static async getCurrentTermsVersion(): Promise {
+ await this.simulateDelay();
+
+ // Return the latest version
+ const sorted = [...MOCK_TERMS_VERSIONS].sort((a, b) =>
+ new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
+ );
+
+ return { ...sorted[0] };
+ }
+
+ /**
+ * Get user's terms acceptance status
+ */
+ static async getUserTermsStatus(userId: string): Promise {
+ await this.simulateDelay();
+
+ const currentTerms = await this.getCurrentTermsVersion();
+ const userAcceptances = MOCK_TERMS_ACCEPTANCES[userId] || [];
+
+ // Find latest acceptance
+ const sortedAcceptances = [...userAcceptances].sort((a, b) =>
+ new Date(b.acceptedAt).getTime() - new Date(a.acceptedAt).getTime()
+ );
+
+ const lastAcceptance = sortedAcceptances[0];
+
+ // Check if current version is accepted
+ const hasAcceptedCurrent = userAcceptances.some(
+ acceptance => acceptance.termsVersionId === currentTerms.id
+ );
+
+ // Determine if reacceptance is required
+ const requiresAcceptance = !hasAcceptedCurrent ||
+ (currentTerms.requiresReacceptance && !hasAcceptedCurrent);
+
+ return {
+ hasAcceptedCurrent,
+ currentVersion: currentTerms.version,
+ lastAcceptedVersion: lastAcceptance?.termsVersion,
+ lastAcceptedAt: lastAcceptance?.acceptedAt,
+ requiresAcceptance,
+ };
+ }
+
+ /**
+ * Record terms acceptance
+ */
+ static async acceptTerms(
+ userId: string,
+ termsVersionId: string,
+ metadata: {
+ ipAddress: string;
+ userAgent: string;
+ }
+ ): Promise {
+ await this.simulateDelay();
+
+ // Verify terms version exists
+ const termsVersion = MOCK_TERMS_VERSIONS.find(v => v.id === termsVersionId);
+ if (!termsVersion) {
+ throw new Error('Terms version not found');
+ }
+
+ // Check if already accepted
+ const existingAcceptances = MOCK_TERMS_ACCEPTANCES[userId] || [];
+ const alreadyAccepted = existingAcceptances.some(
+ a => a.termsVersionId === termsVersionId
+ );
+
+ if (alreadyAccepted) {
+ // Return existing acceptance
+ return existingAcceptances.find(a => a.termsVersionId === termsVersionId)!;
+ }
+
+ // Create new acceptance record
+ const acceptance: TermsAcceptance = {
+ id: `acceptance-${userId}-${termsVersionId}-${Date.now()}`,
+ userId,
+ termsVersionId,
+ termsVersion: termsVersion.version,
+ acceptedAt: new Date().toISOString(),
+ ipAddress: metadata.ipAddress,
+ userAgent: metadata.userAgent,
+ };
+
+ // Store acceptance
+ if (!MOCK_TERMS_ACCEPTANCES[userId]) {
+ MOCK_TERMS_ACCEPTANCES[userId] = [];
+ }
+ MOCK_TERMS_ACCEPTANCES[userId].push(acceptance);
+
+ return { ...acceptance };
+ }
+
+ /**
+ * Check if user needs to accept updated terms
+ */
+ static async requiresReacceptance(userId: string): Promise {
+ const status = await this.getUserTermsStatus(userId);
+ return status.requiresAcceptance;
+ }
+
+ /**
+ * Get terms acceptance history for user
+ */
+ static async getAcceptanceHistory(userId: string): Promise {
+ await this.simulateDelay();
+
+ const acceptances = MOCK_TERMS_ACCEPTANCES[userId] || [];
+
+ // Sort by acceptance date (newest first)
+ return [...acceptances].sort((a, b) =>
+ new Date(b.acceptedAt).getTime() - new Date(a.acceptedAt).getTime()
+ );
+ }
+
+ /**
+ * Get terms version by ID
+ */
+ static async getTermsVersion(versionId: string): Promise {
+ await this.simulateDelay();
+
+ const version = MOCK_TERMS_VERSIONS.find(v => v.id === versionId);
+ return version ? { ...version } : null;
+ }
+
+ /**
+ * Get all terms versions
+ */
+ static async getAllTermsVersions(): Promise {
+ await this.simulateDelay();
+
+ return [...MOCK_TERMS_VERSIONS].sort((a, b) =>
+ new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
+ );
+ }
+
+ /**
+ * Create new terms version (admin function)
+ */
+ static async createTermsVersion(
+ version: Omit
+ ): Promise {
+ await this.simulateDelay();
+
+ const newVersion: TermsVersion = {
+ ...version,
+ id: `terms-v${MOCK_TERMS_VERSIONS.length + 1}`,
+ createdAt: new Date().toISOString(),
+ };
+
+ MOCK_TERMS_VERSIONS.push(newVersion);
+ return { ...newVersion };
+ }
+
+ /**
+ * Simulate database delay
+ */
+ private static async simulateDelay(): Promise {
+ await new Promise(resolve => setTimeout(resolve, 100));
+ }
+
+ /**
+ * Clear all data (for testing)
+ */
+ static clearAllData(): void {
+ Object.keys(MOCK_TERMS_ACCEPTANCES).forEach(key => delete MOCK_TERMS_ACCEPTANCES[key]);
+ MOCK_TERMS_VERSIONS.length = 1; // Keep only the first version
+ }
+}
diff --git a/lib/services/verification.ts b/lib/services/verification.ts
new file mode 100644
index 0000000..a2a0e1f
--- /dev/null
+++ b/lib/services/verification.ts
@@ -0,0 +1,304 @@
+import {
+ VerificationRequest,
+ VerificationDocument,
+ KYCTier,
+ VerificationStatus,
+ DocumentType,
+} from "@/types/compliance";
+import { ComplianceService } from "./compliance";
+import { EmailService } from "./email";
+
+// Mock Data Store
+const MOCK_VERIFICATION_REQUESTS: Record = {};
+const MOCK_DOCUMENTS: Record = {};
+
+export class VerificationService {
+ /**
+ * Create new verification request
+ */
+ static async createVerificationRequest(
+ userId: string,
+ targetTier: KYCTier
+ ): Promise {
+ await this.simulateDelay();
+
+ // Check if user already has a pending request
+ const existing = Object.values(MOCK_VERIFICATION_REQUESTS).find(
+ req => req.userId === userId && req.status === 'PENDING'
+ );
+
+ if (existing) {
+ throw new Error('You already have a pending verification request');
+ }
+
+ // Get current compliance to validate tier upgrade
+ const compliance = await ComplianceService.getUserCompliance(userId);
+ const tiers: KYCTier[] = ['UNVERIFIED', 'BASIC', 'VERIFIED', 'ENHANCED'];
+ const currentIndex = tiers.indexOf(compliance.currentTier);
+ const targetIndex = tiers.indexOf(targetTier);
+
+ if (targetIndex <= currentIndex) {
+ throw new Error('Target tier must be higher than current tier');
+ }
+
+ // Create verification request
+ const requestId = `vreq-${userId}-${Date.now()}`;
+ const request: VerificationRequest = {
+ id: requestId,
+ userId,
+ targetTier,
+ status: 'PENDING',
+ submittedAt: new Date().toISOString(),
+ documents: [],
+ };
+
+ MOCK_VERIFICATION_REQUESTS[requestId] = request;
+ MOCK_DOCUMENTS[requestId] = [];
+
+ // Update user's verification status
+ await ComplianceService.updateVerificationStatus(userId, 'PENDING');
+
+ return { ...request };
+ }
+
+ /**
+ * Get user's current verification status
+ */
+ static async getVerificationStatus(userId: string): Promise {
+ await this.simulateDelay();
+
+ // Find most recent verification request for user
+ const requests = Object.values(MOCK_VERIFICATION_REQUESTS)
+ .filter(req => req.userId === userId)
+ .sort((a, b) => new Date(b.submittedAt).getTime() - new Date(a.submittedAt).getTime());
+
+ if (requests.length === 0) {
+ return null;
+ }
+
+ const request = requests[0];
+
+ // Attach documents
+ const documents = MOCK_DOCUMENTS[request.id] || [];
+ return {
+ ...request,
+ documents: [...documents],
+ };
+ }
+
+ /**
+ * Upload verification document
+ */
+ static async uploadDocument(
+ requestId: string,
+ document: {
+ type: DocumentType;
+ fileName: string;
+ file: File | Blob;
+ }
+ ): Promise {
+ await this.simulateDelay();
+
+ const request = MOCK_VERIFICATION_REQUESTS[requestId];
+ if (!request) {
+ throw new Error('Verification request not found');
+ }
+
+ if (request.status !== 'PENDING') {
+ throw new Error('Cannot upload documents to non-pending request');
+ }
+
+ // Simulate file upload (in real implementation, upload to S3/cloud storage)
+ const fileUrl = `https://storage.boundless.fi/documents/${requestId}/${document.fileName}`;
+
+ // Create document record
+ const doc: VerificationDocument = {
+ id: `doc-${requestId}-${Date.now()}`,
+ type: document.type,
+ fileName: document.fileName,
+ fileUrl,
+ uploadedAt: new Date().toISOString(),
+ verified: false,
+ };
+
+ // Store document
+ if (!MOCK_DOCUMENTS[requestId]) {
+ MOCK_DOCUMENTS[requestId] = [];
+ }
+ MOCK_DOCUMENTS[requestId].push(doc);
+
+ // Update request's documents array
+ request.documents = MOCK_DOCUMENTS[requestId];
+
+ return { ...doc };
+ }
+
+ /**
+ * Update verification request status (admin/automated function)
+ */
+ static async updateVerificationStatus(
+ requestId: string,
+ status: VerificationStatus,
+ reason?: string
+ ): Promise {
+ await this.simulateDelay();
+
+ const request = MOCK_VERIFICATION_REQUESTS[requestId];
+ if (!request) {
+ throw new Error('Verification request not found');
+ }
+
+ // Update request status
+ request.status = status;
+ request.reviewedAt = new Date().toISOString();
+
+ if (status === 'REJECTED' && reason) {
+ request.rejectionReason = reason;
+ }
+
+ // If approved, upgrade user's tier
+ if (status === 'APPROVED') {
+ await ComplianceService.upgradeTier(request.userId, request.targetTier);
+ }
+
+ // Update user's verification status in compliance
+ await ComplianceService.updateVerificationStatus(request.userId, status);
+
+ // Send notification (in real implementation)
+ await this.notifyStatusChange(request.userId, status);
+
+ return true;
+ }
+
+ /**
+ * Delete/remove a document
+ */
+ static async deleteDocument(requestId: string, documentId: string): Promise {
+ await this.simulateDelay();
+
+ const request = MOCK_VERIFICATION_REQUESTS[requestId];
+ if (!request || request.status !== 'PENDING') {
+ throw new Error('Cannot delete documents from non-pending request');
+ }
+
+ const documents = MOCK_DOCUMENTS[requestId] || [];
+ const index = documents.findIndex(doc => doc.id === documentId);
+
+ if (index === -1) {
+ throw new Error('Document not found');
+ }
+
+ // Remove document
+ documents.splice(index, 1);
+ request.documents = documents;
+
+ return true;
+ }
+
+ /**
+ * Get required document types for a tier
+ */
+ static getRequiredDocuments(targetTier: KYCTier): DocumentType[] {
+ switch (targetTier) {
+ case 'BASIC':
+ return ['GOVERNMENT_ID'];
+ case 'VERIFIED':
+ return ['GOVERNMENT_ID', 'PROOF_OF_ADDRESS'];
+ case 'ENHANCED':
+ return ['GOVERNMENT_ID', 'PROOF_OF_ADDRESS', 'SELFIE'];
+ default:
+ return [];
+ }
+ }
+
+ /**
+ * Check if verification request has all required documents
+ */
+ static hasRequiredDocuments(request: VerificationRequest): boolean {
+ const required = this.getRequiredDocuments(request.targetTier);
+ const uploaded = request.documents.map(doc => doc.type);
+
+ return required.every(type => uploaded.includes(type));
+ }
+
+ /**
+ * Send status change notification
+ */
+ static async notifyStatusChange(
+ userId: string,
+ newStatus: VerificationStatus
+ ): Promise {
+ const userEmail = `user-${userId}@example.com`;
+ const request = Object.values(MOCK_VERIFICATION_REQUESTS)
+ .find(req => req.userId === userId && req.status === newStatus);
+
+ await EmailService.sendVerificationStatusEmail(
+ userEmail,
+ newStatus,
+ request?.targetTier,
+ request?.rejectionReason
+ );
+ }
+
+ /**
+ * Get all verification requests for user (history)
+ */
+ static async getVerificationHistory(userId: string): Promise {
+ await this.simulateDelay();
+
+ const requests = Object.values(MOCK_VERIFICATION_REQUESTS)
+ .filter(req => req.userId === userId)
+ .sort((a, b) => new Date(b.submittedAt).getTime() - new Date(a.submittedAt).getTime());
+
+ // Attach documents to each request
+ return requests.map(req => ({
+ ...req,
+ documents: [...(MOCK_DOCUMENTS[req.id] || [])],
+ }));
+ }
+
+ /**
+ * Cancel pending verification request
+ */
+ static async cancelVerificationRequest(requestId: string, userId: string): Promise {
+ await this.simulateDelay();
+
+ const request = MOCK_VERIFICATION_REQUESTS[requestId];
+ if (!request) {
+ throw new Error('Verification request not found');
+ }
+
+ if (request.userId !== userId) {
+ throw new Error('Unauthorized');
+ }
+
+ if (request.status !== 'PENDING') {
+ throw new Error('Can only cancel pending requests');
+ }
+
+ // Update status
+ request.status = 'REJECTED';
+ request.rejectionReason = 'Cancelled by user';
+ request.reviewedAt = new Date().toISOString();
+
+ // Update compliance status
+ await ComplianceService.updateVerificationStatus(userId, 'NOT_STARTED');
+
+ return true;
+ }
+
+ /**
+ * Simulate database delay
+ */
+ private static async simulateDelay(ms: number = 100): Promise {
+ await new Promise(resolve => setTimeout(resolve, ms));
+ }
+
+ /**
+ * Clear all data (for testing)
+ */
+ static clearAllData(): void {
+ Object.keys(MOCK_VERIFICATION_REQUESTS).forEach(key => delete MOCK_VERIFICATION_REQUESTS[key]);
+ Object.keys(MOCK_DOCUMENTS).forEach(key => delete MOCK_DOCUMENTS[key]);
+ }
+}
diff --git a/lib/services/withdrawal.ts b/lib/services/withdrawal.ts
new file mode 100644
index 0000000..5958bb9
--- /dev/null
+++ b/lib/services/withdrawal.ts
@@ -0,0 +1,101 @@
+import { WithdrawalRequest, WithdrawalValidationResult } from "@/types/withdrawal";
+import { ComplianceService } from "./compliance";
+import { TermsService } from "./terms";
+import { GeoRestrictionService } from "./geo-restriction";
+import { mockWalletWithAssets } from "@/lib/mock-wallet";
+
+const MOCK_WITHDRAWALS: Record = {};
+
+export class WithdrawalService {
+ static async validate(userId: string, amount: number, ip: string): Promise {
+ const result: WithdrawalValidationResult = {
+ valid: true,
+ errors: [],
+ warnings: [],
+ blockers: {},
+ };
+
+ // Check balance
+ if (amount > mockWalletWithAssets.balance) {
+ result.valid = false;
+ result.errors.push('Insufficient balance');
+ result.blockers.insufficientBalance = true;
+ }
+
+ // Check limits
+ const compliance = await ComplianceService.getUserCompliance(userId);
+ const limitCheck = await ComplianceService.validateWithdrawalAmount(userId, amount);
+
+ if (!limitCheck.valid) {
+ result.valid = false;
+ result.errors.push(`Exceeds ${limitCheck.exceededLimit} limit`);
+ result.blockers.exceedsLimit = true;
+ result.blockers.limitType = limitCheck.exceededLimit;
+ }
+
+ // Check hold state
+ if (compliance.holdState !== 'NONE') {
+ result.valid = false;
+ result.errors.push(`Account is ${compliance.holdState.toLowerCase()}`);
+ result.blockers.complianceHold = true;
+ }
+
+ // Check terms
+ const termsStatus = await TermsService.getUserTermsStatus(userId);
+ if (termsStatus.requiresAcceptance) {
+ result.valid = false;
+ result.errors.push('Terms must be accepted');
+ result.blockers.termsNotAccepted = true;
+ }
+
+ // Check location
+ const location = await GeoRestrictionService.checkLocation(ip);
+ if (location.isRestricted) {
+ result.valid = false;
+ result.errors.push('Withdrawals not available in your region');
+ result.blockers.restrictedJurisdiction = true;
+ }
+
+ return result;
+ }
+
+ static async submit(userId: string, amount: number, currency: string, destinationId: string, ip: string): Promise {
+ const validation = await this.validate(userId, amount, ip);
+ if (!validation.valid) {
+ throw new Error(validation.errors[0] || 'Withdrawal validation failed');
+ }
+
+ const compliance = await ComplianceService.getUserCompliance(userId);
+
+ const withdrawal: WithdrawalRequest = {
+ id: `wd-${Date.now()}`,
+ userId,
+ amount,
+ currency,
+ destinationId,
+ fee: 2.50,
+ netAmount: amount - 2.50,
+ status: 'PENDING',
+ compliance: {
+ tierAtSubmission: compliance.currentTier,
+ limitsChecked: true,
+ termsAccepted: true,
+ geoCheckPassed: true,
+ },
+ createdAt: new Date().toISOString(),
+ };
+
+ if (!MOCK_WITHDRAWALS[userId]) {
+ MOCK_WITHDRAWALS[userId] = [];
+ }
+ MOCK_WITHDRAWALS[userId].push(withdrawal);
+
+ await ComplianceService.trackWithdrawal(userId, amount);
+
+ return withdrawal;
+ }
+
+ static async getHistory(userId: string): Promise {
+ return MOCK_WITHDRAWALS[userId] || [];
+ }
+}
diff --git a/package-lock.json b/package-lock.json
index 28656d5..55d5417 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -174,7 +174,7 @@
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
"integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@csstools/css-calc": "^2.1.3",
@@ -188,7 +188,7 @@
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
- "dev": true,
+ "devOptional": true,
"license": "ISC"
},
"node_modules/@babel/code-frame": {
@@ -1267,7 +1267,7 @@
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
"integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
- "dev": true,
+ "devOptional": true,
"funding": [
{
"type": "github",
@@ -1287,7 +1287,7 @@
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
"integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
- "dev": true,
+ "devOptional": true,
"funding": [
{
"type": "github",
@@ -1311,7 +1311,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
"integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
- "dev": true,
+ "devOptional": true,
"funding": [
{
"type": "github",
@@ -1339,7 +1339,7 @@
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
"integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
- "dev": true,
+ "devOptional": true,
"funding": [
{
"type": "github",
@@ -1362,7 +1362,7 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
"integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
- "dev": true,
+ "devOptional": true,
"funding": [
{
"type": "github",
@@ -1468,7 +1468,6 @@
"cpu": [
"ppc64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1485,7 +1484,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1502,7 +1500,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1519,7 +1516,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1536,7 +1532,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1553,7 +1548,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1570,7 +1564,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1587,7 +1580,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1604,7 +1596,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1621,7 +1612,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1638,7 +1628,6 @@
"cpu": [
"ia32"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1655,7 +1644,6 @@
"cpu": [
"loong64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1672,7 +1660,6 @@
"cpu": [
"mips64el"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1689,7 +1676,6 @@
"cpu": [
"ppc64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1706,7 +1692,6 @@
"cpu": [
"riscv64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1723,7 +1708,6 @@
"cpu": [
"s390x"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1740,7 +1724,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1757,7 +1740,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1774,7 +1756,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1791,7 +1772,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1808,7 +1788,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1825,7 +1804,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1842,7 +1820,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1859,7 +1836,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1876,7 +1852,6 @@
"cpu": [
"ia32"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1893,7 +1868,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -7149,7 +7123,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -7163,7 +7136,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -7177,7 +7149,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -7191,7 +7162,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -7205,7 +7175,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -7219,7 +7188,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -7233,7 +7201,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -7247,7 +7214,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -7261,7 +7227,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -7275,7 +7240,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -7289,7 +7253,6 @@
"cpu": [
"loong64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -7303,7 +7266,6 @@
"cpu": [
"loong64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -7317,7 +7279,6 @@
"cpu": [
"ppc64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -7331,7 +7292,6 @@
"cpu": [
"ppc64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -7345,7 +7305,6 @@
"cpu": [
"riscv64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -7359,7 +7318,6 @@
"cpu": [
"riscv64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -7373,7 +7331,6 @@
"cpu": [
"s390x"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -7387,7 +7344,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -7401,7 +7357,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -7415,7 +7370,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -7429,7 +7383,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -7443,7 +7396,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -7457,7 +7409,6 @@
"cpu": [
"ia32"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -7471,7 +7422,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -7485,7 +7435,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -8316,7 +8265,7 @@
"version": "20.19.29",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.29.tgz",
"integrity": "sha512-YrT9ArrGaHForBaCNwFjoqJWmn8G1Pr7+BH/vwyLHciA9qT/wSiuOhxGCT50JA5xLvFBd6PIiGkE3afxcPE1nw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
@@ -9188,7 +9137,7 @@
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"engines": {
"node": ">= 14"
@@ -10567,7 +10516,7 @@
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
"integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@asamuzakjp/css-color": "^3.2.0",
@@ -10725,7 +10674,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
"integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"whatwg-mimetype": "^4.0.0",
@@ -10856,7 +10805,7 @@
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/decimal.js-light": {
@@ -11231,7 +11180,7 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
- "dev": true,
+ "devOptional": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
@@ -12368,7 +12317,6 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -13209,7 +13157,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
"integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"whatwg-encoding": "^3.1.1"
@@ -13239,7 +13187,7 @@
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.0",
@@ -13253,7 +13201,7 @@
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
@@ -13277,7 +13225,7 @@
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
@@ -13816,7 +13764,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/is-regex": {
@@ -15154,7 +15102,7 @@
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
@@ -15192,7 +15140,7 @@
"version": "26.1.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"cssstyle": "^4.2.1",
@@ -15379,7 +15327,7 @@
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
"integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
- "dev": true,
+ "devOptional": true,
"license": "MPL-2.0",
"dependencies": {
"detect-libc": "^2.0.3"
@@ -15412,7 +15360,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -15433,7 +15380,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -15454,7 +15400,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -15475,7 +15420,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -15496,7 +15440,6 @@
"cpu": [
"arm"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -15517,7 +15460,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -15538,7 +15480,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -15559,7 +15500,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -15580,7 +15520,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -15601,7 +15540,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -15622,7 +15560,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -17066,7 +17003,7 @@
"version": "2.2.23",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz",
"integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/object-assign": {
@@ -17412,7 +17349,7 @@
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"entities": "^6.0.0"
@@ -17781,7 +17718,7 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -18364,7 +18301,7 @@
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/run-parallel": {
@@ -18460,14 +18397,14 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/saxes": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
- "dev": true,
+ "devOptional": true,
"license": "ISC",
"dependencies": {
"xmlchars": "^2.2.0"
@@ -19362,7 +19299,7 @@
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/sync-fetch": {
@@ -19588,7 +19525,7 @@
"version": "6.1.86",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
"integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"tldts-core": "^6.1.86"
@@ -19601,7 +19538,7 @@
"version": "6.1.86",
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
"integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/tmpl": {
@@ -19628,7 +19565,7 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
"integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
- "dev": true,
+ "devOptional": true,
"license": "BSD-3-Clause",
"dependencies": {
"tldts": "^6.1.32"
@@ -19641,7 +19578,7 @@
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"punycode": "^2.3.1"
@@ -19988,7 +19925,7 @@
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/unified": {
@@ -20552,7 +20489,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"xml-name-validator": "^5.0.0"
@@ -20585,7 +20522,7 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
- "dev": true,
+ "devOptional": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
@@ -20596,7 +20533,7 @@
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
"deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"iconv-lite": "0.6.3"
@@ -20609,7 +20546,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=18"
@@ -20619,7 +20556,7 @@
"version": "14.2.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"tr46": "^5.1.0",
@@ -20899,7 +20836,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
- "dev": true,
+ "devOptional": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18"
@@ -20909,7 +20846,7 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/xmlhttprequest-ssl": {
@@ -20941,7 +20878,7 @@
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
- "dev": true,
+ "devOptional": true,
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
diff --git a/types/compliance.ts b/types/compliance.ts
new file mode 100644
index 0000000..184b2ad
--- /dev/null
+++ b/types/compliance.ts
@@ -0,0 +1,98 @@
+// KYC Verification Tiers
+export type KYCTier = 'UNVERIFIED' | 'BASIC' | 'VERIFIED' | 'ENHANCED';
+
+// Compliance Hold States
+export type ComplianceHoldState = 'NONE' | 'REVIEW' | 'SUSPENDED' | 'BLOCKED';
+
+// Verification Status
+export type VerificationStatus = 'NOT_STARTED' | 'PENDING' | 'APPROVED' | 'REJECTED';
+
+// Verification Document Types
+export type DocumentType = 'GOVERNMENT_ID' | 'PROOF_OF_ADDRESS' | 'SELFIE' | 'ADDITIONAL';
+
+export interface WithdrawalLimits {
+ daily: number;
+ weekly: number;
+ monthly: number;
+ perTransaction: number;
+}
+
+export interface WithdrawalUsage {
+ dailyUsed: number;
+ weeklyUsed: number;
+ monthlyUsed: number;
+ lifetimeTotal: number;
+ lastResetDaily: string;
+ lastResetWeekly: string;
+ lastResetMonthly: string;
+}
+
+export interface KYCTierConfig {
+ tier: KYCTier;
+ limits: WithdrawalLimits;
+ requirements: string[];
+ processingTime?: string;
+ description: string;
+}
+
+export interface UserCompliance {
+ userId: string;
+ currentTier: KYCTier;
+ limits: WithdrawalLimits;
+ usage: WithdrawalUsage;
+ holdState: ComplianceHoldState;
+ holdReason?: string;
+ verificationStatus: VerificationStatus;
+ restrictedJurisdiction: boolean;
+ jurisdictionCode?: string;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface VerificationRequest {
+ id: string;
+ userId: string;
+ targetTier: KYCTier;
+ status: VerificationStatus;
+ submittedAt: string;
+ reviewedAt?: string;
+ rejectionReason?: string;
+ documents: VerificationDocument[];
+ notes?: string;
+}
+
+export interface VerificationDocument {
+ id: string;
+ type: DocumentType;
+ fileName: string;
+ fileUrl: string;
+ uploadedAt: string;
+ verified: boolean;
+}
+
+// Helper type for remaining limits calculation
+export interface RemainingLimits {
+ daily: number;
+ weekly: number;
+ monthly: number;
+ percentUsed: {
+ daily: number;
+ weekly: number;
+ monthly: number;
+ };
+}
+
+// Appeal types
+export type AppealStatus = 'PENDING' | 'UNDER_REVIEW' | 'APPROVED' | 'DENIED';
+
+export interface VerificationAppeal {
+ id: string;
+ userId: string;
+ verificationRequestId: string;
+ reason: string;
+ additionalInfo?: string;
+ status: AppealStatus;
+ submittedAt: string;
+ reviewedAt?: string;
+ reviewNotes?: string;
+}
diff --git a/types/geography.ts b/types/geography.ts
new file mode 100644
index 0000000..b7698f9
--- /dev/null
+++ b/types/geography.ts
@@ -0,0 +1,20 @@
+export interface RestrictedJurisdiction {
+ code: string; // ISO country code or US state code
+ name: string;
+ type: 'COUNTRY' | 'STATE';
+ reason: string;
+ effectiveDate: string;
+}
+
+export interface UserLocation {
+ ip: string;
+ countryCode: string;
+ countryName: string;
+ regionCode?: string;
+ regionName?: string;
+ city?: string;
+ isVPN: boolean;
+ isProxy: boolean;
+ isRestricted: boolean;
+ restrictionReason?: string;
+}
diff --git a/types/terms.ts b/types/terms.ts
new file mode 100644
index 0000000..99b1abf
--- /dev/null
+++ b/types/terms.ts
@@ -0,0 +1,28 @@
+export interface TermsVersion {
+ id: string;
+ version: string;
+ title: string;
+ content: string;
+ summary?: string;
+ effectiveDate: string;
+ requiresReacceptance: boolean;
+ createdAt: string;
+}
+
+export interface TermsAcceptance {
+ id: string;
+ userId: string;
+ termsVersionId: string;
+ termsVersion: string;
+ acceptedAt: string;
+ ipAddress: string;
+ userAgent: string;
+}
+
+export interface UserTermsStatus {
+ hasAcceptedCurrent: boolean;
+ currentVersion: string;
+ lastAcceptedVersion?: string;
+ lastAcceptedAt?: string;
+ requiresAcceptance: boolean;
+}
diff --git a/types/withdrawal.ts b/types/withdrawal.ts
new file mode 100644
index 0000000..b3a4f83
--- /dev/null
+++ b/types/withdrawal.ts
@@ -0,0 +1,46 @@
+import { KYCTier } from './compliance';
+
+export interface WithdrawalRequest {
+ id: string;
+ userId: string;
+ amount: number;
+ currency: string;
+ destinationId: string; // Bank account ID
+ destinationName?: string;
+ fee: number;
+ netAmount: number;
+ status: 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED' | 'CANCELLED';
+ compliance: {
+ tierAtSubmission: KYCTier;
+ limitsChecked: boolean;
+ termsAccepted: boolean;
+ geoCheckPassed: boolean;
+ };
+ createdAt: string;
+ processedAt?: string;
+ completedAt?: string;
+ failureReason?: string;
+}
+
+export interface WithdrawalValidationResult {
+ valid: boolean;
+ errors: string[];
+ warnings: string[];
+ blockers: {
+ insufficientBalance?: boolean;
+ exceedsLimit?: boolean;
+ limitType?: 'daily' | 'weekly' | 'monthly' | 'perTransaction';
+ complianceHold?: boolean;
+ restrictedJurisdiction?: boolean;
+ termsNotAccepted?: boolean;
+ unverifiedAccount?: boolean;
+ };
+}
+
+export interface WithdrawalHistoryFilters {
+ startDate?: string;
+ endDate?: string;
+ status?: WithdrawalRequest['status'];
+ limit?: number;
+ offset?: number;
+}