Compliance Sanctions Screener
blocklegalAML/KYC OFAC, Politically Exposed Persons (PEP), and global sanctions list screening workbench with fuzzy match threshold tuning, real-time entity inspection modal, and prohibited match enforcement.
Installation
$pnpm dlx uipkge-ng@latest add compliance-sanctions-screener$npx uipkge-ng@latest add compliance-sanctions-screener$yarn dlx uipkge-ng@latest add compliance-sanctions-screener$bunx uipkge-ng@latest add compliance-sanctions-screenerManifest:
https://uipkge.dev/r/angular/compliance-sanctions-screener.jsonInstalls to:src/app/components/blocks/compliance-sanctions-screener/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
SanctionAttributeComparisoninterface SanctionAttributeComparison {
field: string
submittedValue: string
watchlistValue: string
matchType: 'exact' | 'fuzzy' | 'partial' | 'clean' | 'unmatched'
score: number
note?: string
}SanctionRecordinterface SanctionRecord {
id: string
entityName: string
originalScript?: string
entityType: EntityType
matchScore: number
matchStatus: MatchStatus
sanctionList: string
program: string
matchedAttributesSummary: string
country: string
jurisdiction: string
dateOfBirthOrIncorporation: string
nationalIdOrLei: string
address: string
aliases: string[]
pepLevel?: string
remarks?: string
lastScreened: string
attributeComparisons: SanctionAttributeComparison[]
}NotificationBannerinterface NotificationBanner {
message: string
type: 'success' | 'warning' | 'destructive' | 'info'
}Files installed (5)
src/app/components/blocks/compliance-sanctions-screener/compliance-sanctions-screener.component.ts36 kB
// AML/KYC OFAC, Politically Exposed Persons (PEP), and global sanctions list screening // workbench with fuzzy match threshold tuning, real-time entity inspection modal, and // prohibited match enforcement. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core' import { AlertTriangle, Ban, Building2, Check, CheckCircle2, Eye, FileSearch, Globe, Info, LucideAngularModule, RefreshCw, RotateCcw, Search, ShieldAlert, ShieldCheck, ShieldX, Sliders, User, Users, X, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent } from '@/ui/card/card.component' import { UiInputComponent } from '@/ui/input/input.component' import { UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, } from '@/ui/select/select.component' import { UiSliderComponent } from '@/ui/slider/slider.component' import { UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, } from '@/ui/table/table.component' import type { EntityType, SanctionRecord } from './compliance-sanctions-types' import { defaultSanctionRecords } from './compliance-sanctions-data' import { UiSanctionsInvestigationDialogComponent } from './sanctions-investigation-dialog.component' export type { EntityType, SanctionRecord } function cloneRecords(records: SanctionRecord[]): SanctionRecord[] { return JSON.parse(JSON.stringify(records)) as SanctionRecord[] } @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-compliance-sanctions-screener, [ui-compliance-sanctions-screener]', standalone: true, // React renders the root <div> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiInputComponent, UiSanctionsInvestigationDialogComponent, UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, UiSliderComponent, UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, ], template: ` <div data-slot="compliance-sanctions-screener" [class]="rootClass"> <!-- Header Section --> <div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between"> <div> <div class="flex flex-wrap items-center gap-2.5"> <h1 class="text-xl font-bold tracking-tight sm:text-2xl">AML / OFAC Sanctions & PEP Screening</h1> <span ui-badge variant="outline" class="border-success/30 bg-success/10 text-success gap-1.5 py-0.5"> <span class="bg-success size-1.5 animate-pulse rounded-full"></span> <span class="font-medium">OFAC SDN, EU Consolidated, UK HMT, UN Sanctions · Synced 10m ago</span> </span> </div> <p class="text-muted-foreground mt-1 text-xs sm:text-sm"> Automated identity verification, Politically Exposed Persons (PEP), and multilateral sanctions screening workbench. </p> </div> <div class="flex shrink-0 items-center gap-2"> <button ui-button variant="default" size="sm" [disabled]="isBatchScreening()" class="cursor-pointer gap-2 shadow-xs" (click)="handleRunBatchScreening()" > <lucide-icon [img]="RefreshCw" [class]="batchIconClass" /> <span>{{ isBatchScreening() ? 'Screening 1,420 Records...' : 'Run Batch Screening' }}</span> </button> </div> </div> <!-- Notification Toast / Banner --> @if (notificationBanner(); as banner) { <div [class]="bannerClass(banner.type)"> <div class="flex min-w-0 items-center gap-2"> @switch (banner.type) { @case ('success') { <lucide-icon [img]="CheckCircle2" class="size-4 shrink-0" /> } @case ('warning') { <lucide-icon [img]="AlertTriangle" class="size-4 shrink-0" /> } @case ('destructive') { <lucide-icon [img]="Ban" class="size-4 shrink-0" /> } @default { <lucide-icon [img]="Info" class="size-4 shrink-0" /> } } <span>{{ banner.message }}</span> </div> <button aria-label="Dismiss notification" type="button" class="rounded-md p-1 transition-colors hover:bg-black/5 dark:hover:bg-white/10" (click)="notificationBanner.set(null)" > <lucide-icon [img]="X" class="size-3.5" /> </button> </div> } <!-- 4 Screening Telemetry Cards --> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> <!-- Card 1: Total Screened --> <div ui-card class="border-border/80 border shadow-xs"> <div ui-card-content class="p-4 sm:p-5"> <div class="flex items-center justify-between"> <span class="text-muted-foreground text-xs font-medium tracking-wider uppercase" >Total Screened Today</span > <div class="bg-muted text-muted-foreground flex size-8 items-center justify-center rounded-md"> <lucide-icon [img]="Users" class="size-4" /> </div> </div> <div class="mt-3 flex items-baseline gap-2"> <span class="text-2xl font-bold tracking-tight tabular-nums">{{ totalScreenedCount.toLocaleString() }}</span> <span class="text-muted-foreground text-xs font-medium">Entities</span> </div> <p class="text-muted-foreground mt-1 text-xs"> <span class="text-success font-medium">+14.2%</span> vs yesterday · 38 automated batches </p> </div> </div> <!-- Card 2: Clear / No Matches --> <div ui-card class="border-border/80 border shadow-xs"> <div ui-card-content class="p-4 sm:p-5"> <div class="flex items-center justify-between"> <span class="text-muted-foreground text-xs font-medium tracking-wider uppercase">Clear / No Matches</span> <div class="bg-success/10 text-success flex size-8 items-center justify-center rounded-md"> <lucide-icon [img]="ShieldCheck" class="size-4" /> </div> </div> <div class="mt-3 flex items-baseline gap-2"> <span class="text-success text-2xl font-bold tracking-tight tabular-nums"> {{ clearedCount().toLocaleString() }} </span> <span class="text-success text-xs font-semibold tabular-nums">Cleared · 99.2%</span> </div> <p class="text-muted-foreground mt-1 text-xs">Low Risk · 0 PEP matches · 0 adverse media</p> </div> </div> <!-- Card 3: Potential Matches Flagged --> <div ui-card class="border-border/80 border shadow-xs"> <div ui-card-content class="p-4 sm:p-5"> <div class="flex items-center justify-between"> <span class="text-muted-foreground text-xs font-medium tracking-wider uppercase" >Potential Matches Flagged</span > <div class="bg-warning/10 text-warning flex size-8 items-center justify-center rounded-md"> <lucide-icon [img]="AlertTriangle" class="size-4" /> </div> </div> <div class="mt-3 flex items-baseline gap-2"> <span class="text-warning text-2xl font-bold tracking-tight tabular-nums"> {{ flaggedCount() }} </span> <span class="text-warning text-xs font-semibold">Pending Review</span> </div> <p class="text-muted-foreground mt-1 text-xs">Action required · SLA <2h · 1 high confidence</p> </div> </div> <!-- Card 4: Confirmed Prohibited Matches --> <div ui-card class="border-border/80 border shadow-xs"> <div ui-card-content class="p-4 sm:p-5"> <div class="flex items-center justify-between"> <span class="text-muted-foreground text-xs font-medium tracking-wider uppercase" >Confirmed Prohibited Matches</span > <div class="bg-destructive/10 text-destructive flex size-8 items-center justify-center rounded-md"> <lucide-icon [img]="Ban" class="size-4" /> </div> </div> <div class="mt-3 flex items-baseline gap-2"> <span class="text-2xl font-bold tracking-tight tabular-nums" [class]="blockedCount() > 0 ? 'text-destructive' : 'text-foreground'" > {{ blockedCount() }} </span> <span class="text-muted-foreground text-xs font-semibold">Blocked</span> </div> <p class="text-muted-foreground mt-1 text-xs">Asset freeze locked · FinCEN SAR ready</p> </div> </div> </div> <!-- Real-Time Entity Search Bar & Fuzzy Threshold Workbench --> <div ui-card class="border-border/80 bg-card border shadow-xs"> <div ui-card-content class="space-y-4 p-4 sm:p-5"> <div class="grid grid-cols-1 items-end gap-4 md:grid-cols-12"> <!-- Search Input --> <div class="space-y-1.5 md:col-span-4"> <label for="entity-search-input" class="text-foreground flex items-center gap-1.5 text-xs font-medium"> <lucide-icon [img]="Search" class="text-muted-foreground size-3.5" /> <span>Search Individual / Entity Name</span> </label> <div class="relative"> <ui-input id="entity-search-input" [value]="searchQuery()" (valueChange)="searchQuery.set($event)" type="text" placeholder="Search name, alias, ID or passport..." class="bg-background pl-8 text-xs sm:text-sm" /> <lucide-icon [img]="Search" class="text-muted-foreground pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2" /> @if (searchQuery()) { <button aria-label="Clear search" type="button" class="text-muted-foreground hover:text-foreground absolute top-1/2 right-2.5 -translate-y-1/2" (click)="searchQuery.set('')" > <lucide-icon [img]="X" class="size-3.5" /> </button> } </div> </div> <!-- Country Filter Dropdown --> <div class="space-y-1.5 md:col-span-3"> <span class="text-foreground flex items-center gap-1.5 text-xs font-medium"> <lucide-icon [img]="Globe" class="text-muted-foreground size-3.5" /> <span>Country / Domicile Filter</span> </span> <ui-select [value]="selectedCountry()" (valueChange)="selectedCountry.set($event)"> <button ui-select-trigger class="bg-background w-full text-xs sm:text-sm"> <ui-select-value placeholder="All Countries" /> </button> <ui-select-content> <ui-select-item value="all">All Countries</ui-select-item> <ui-select-item value="Russia">Russia (Russian Federation)</ui-select-item> <ui-select-item value="United States">United States (USA)</ui-select-item> <ui-select-item value="Cyprus">Cyprus (EU)</ui-select-item> <ui-select-item value="United Kingdom">United Kingdom (UK)</ui-select-item> <ui-select-item value="Switzerland">Switzerland (CH)</ui-select-item> <ui-select-item value="United Arab Emirates">United Arab Emirates (UAE)</ui-select-item> </ui-select-content> </ui-select> </div> <!-- Entity Type Pills --> <div class="space-y-1.5 md:col-span-2"> <span class="text-foreground flex items-center gap-1.5 text-xs font-medium"> <lucide-icon [img]="Building2" class="text-muted-foreground size-3.5" /> <span>Entity Type</span> </span> <div class="bg-muted border-input flex h-9 items-center gap-1 rounded-md border p-1"> <button type="button" [class]="entityPillClass('all')" (click)="selectedEntityType.set('all')"> All </button> <button type="button" [class]="entityPillClass('individual')" (click)="selectedEntityType.set('individual')" > Indiv. </button> <button type="button" [class]="entityPillClass('corporate')" (click)="selectedEntityType.set('corporate')" > Corp. </button> </div> </div> <!-- Match Threshold Slider --> <div class="space-y-1.5 md:col-span-3"> <div class="flex flex-wrap items-center justify-between gap-x-2 gap-y-1"> <span class="text-foreground flex items-center gap-1.5 text-xs font-medium"> <lucide-icon [img]="Sliders" class="text-muted-foreground size-3.5" /> <span>Match Threshold</span> </span> <span ui-badge variant="secondary" class="text-xs font-semibold tabular-nums"> {{ currentThreshold() }}% Fuzzy Match </span> </div> <div class="pt-1.5"> <ui-slider [value]="thresholdValue()" (valueChange)="onThresholdChange($event)" [min]="50" [max]="100" [step]="1" class="w-full" /> </div> </div> </div> <!-- Filter helper status bar --> <div class="border-border/60 text-muted-foreground flex flex-wrap items-center justify-between gap-2 border-t pt-2 text-xs" > <div class="flex min-w-0 items-center gap-2"> <span >Showing <strong class="text-foreground tabular-nums">{{ filteredRecords().length }}</strong> of <strong class="text-foreground tabular-nums">{{ records().length }}</strong> screening audit records</span > @if (searchQuery() || selectedCountry() !== 'all' || selectedEntityType() !== 'all') { <span class="text-muted-foreground/60">· Filters applied</span> } </div> <div class="flex min-w-0 items-center gap-2"> @if ( searchQuery() || selectedCountry() !== 'all' || selectedEntityType() !== 'all' || currentThreshold() !== 85 ) { <button ui-button variant="ghost" size="sm" class="text-muted-foreground hover:text-foreground h-7 gap-1 text-xs" (click)="handleResetFilters()" > <lucide-icon [img]="RotateCcw" class="size-3" /> <span>Reset Filters</span> </button> } </div> </div> </div> </div> <!-- Screening Match Results Table --> <div ui-card class="border-border/80 overflow-hidden border shadow-xs"> <div class="border-border/60 border-b p-4 pb-3 sm:p-5"> <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"> <div> <h3 class="text-base font-semibold">Active Screening Queue & Target List Matches</h3> <p class="text-muted-foreground mt-0.5 text-xs"> Real-time fuzzy scoring across OFAC SDN, Sectoral SSI, EU Restrictive Measures, and UN Security Council lists. </p> </div> <div class="text-muted-foreground flex items-center gap-1.5 text-xs"> <span class="bg-warning inline-block size-2 rounded-full"></span> <span>Amber: Requires Review</span> <span class="bg-destructive ml-2 inline-block size-2 rounded-full"></span> <span>Red: Prohibited Asset Freeze</span> </div> </div> </div> <div class="overflow-x-auto"> <table ui-table> <thead ui-table-header class="bg-muted/40"> <tr ui-table-row> <th ui-table-head class="min-w-[220px]">Entity Name & Classification</th> <th ui-table-head class="min-w-[140px]">Match Score & Status</th> <th ui-table-head class="min-w-[200px]">Designated Watchlist / Program</th> <th ui-table-head class="min-w-[220px]">Matched Attributes</th> <th ui-table-head class="min-w-[130px]">Jurisdiction</th> <th ui-table-head class="min-w-[220px] text-right">Actions</th> </tr> </thead> <tbody ui-table-body> @for (record of filteredRecords(); track record.id) { <tr ui-table-row [class]="rowClass(record.matchStatus)"> <!-- 1. Entity Name & Classification --> <td ui-table-cell class="py-3.5 align-top font-medium"> <div class="flex items-start gap-2.5"> <div [class]="avatarClass(record.entityType)"> @if (record.entityType === 'individual') { <lucide-icon [img]="User" class="size-4" /> } @else { <lucide-icon [img]="Building2" class="size-4" /> } </div> <div> <div class="flex items-center gap-1.5"> <span class="text-foreground font-semibold">{{ record.entityName }}</span> <span ui-badge variant="outline" class="px-1.5 py-0 text-xs uppercase"> {{ record.entityType }} </span> </div> @if (record.originalScript) { <p class="text-muted-foreground mt-0.5 text-xs"> {{ record.originalScript }} </p> } @if (record.aliases && record.aliases.length > 0) { <div class="text-muted-foreground/80 mt-1 flex flex-wrap gap-1 text-xs"> <span class="font-medium">AKAs:</span> <span>{{ aliasPreview(record) }}</span> </div> } </div> </div> </td> <!-- 2. Match Score & Status --> <td ui-table-cell class="py-3.5 align-top"> <div class="space-y-1"> @if (record.matchStatus === 'blocked') { <div> <span ui-badge variant="destructive" class="gap-1 text-xs font-semibold"> <lucide-icon [img]="Ban" class="size-3" /> <span>Blocked & Frozen</span> </span> </div> } @else if (record.matchStatus === 'cleared_false_positive') { <div> <span ui-badge variant="outline" class="border-success/30 bg-success/10 text-success gap-1 text-xs font-semibold" > <lucide-icon [img]="CheckCircle2" class="size-3" /> <span>Cleared (False Positive)</span> </span> </div> } @else if (record.matchScore >= 95) { <div> <span ui-badge variant="destructive" class="gap-1 text-xs font-semibold tabular-nums"> <lucide-icon [img]="ShieldX" class="size-3" /> <span>{{ record.matchScore }}% Critical Match</span> </span> </div> } @else if (record.matchScore >= currentThreshold()) { <div> <span ui-badge variant="outline" class="border-warning/40 bg-warning/15 text-warning gap-1 text-xs font-semibold tabular-nums" > <lucide-icon [img]="AlertTriangle" class="size-3" /> <span>{{ record.matchScore }}% Match</span> </span> </div> } @else { <div> <span ui-badge variant="outline" class="border-success/30 bg-success/10 text-success gap-1 text-xs font-semibold tabular-nums" > <lucide-icon [img]="Check" class="size-3" /> <span>{{ record.matchScore }}% Match · Clear</span> </span> </div> } <div class="text-muted-foreground text-xs tabular-nums">Screened {{ record.lastScreened }}</div> </div> </td> <!-- 3. Target Sanctions List & Programs --> <td ui-table-cell class="py-3.5 align-top"> <div class="space-y-1"> <div class="text-foreground text-xs font-medium"> {{ record.sanctionList }} </div> <div class="text-muted-foreground line-clamp-2 text-xs"> {{ record.program }} </div> @if (record.pepLevel) { <div class="text-warning text-xs font-medium"> {{ record.pepLevel }} </div> } </div> </td> <!-- 4. Matched Attributes --> <td ui-table-cell class="py-3.5 align-top"> <div class="space-y-1 text-xs"> <div class="text-foreground font-medium"> {{ record.matchedAttributesSummary }} </div> <div class="text-muted-foreground tabular-nums">ID: {{ record.nationalIdOrLei }}</div> <div class="text-muted-foreground max-w-[240px] truncate"> {{ record.address }} </div> </div> </td> <!-- 5. Jurisdiction --> <td ui-table-cell class="py-3.5 align-top"> <div class="space-y-1 text-xs"> <div class="text-foreground flex items-center gap-1 font-medium"> <lucide-icon [img]="Globe" class="text-muted-foreground size-3 shrink-0" /> <span>{{ record.country }}</span> </div> <div class="text-muted-foreground"> {{ record.jurisdiction }} </div> </div> </td> <!-- 6. Actions --> <td ui-table-cell class="py-3.5 text-right align-top"> <div class="flex flex-col items-end justify-end gap-1.5 sm:flex-row sm:items-center"> <button ui-button variant="outline" size="sm" class="h-8 cursor-pointer gap-1.5 text-xs" (click)="openInvestigation(record)" > <lucide-icon [img]="Eye" class="size-3.5" /> <span>Review Match</span> </button> @if ( record.id === 'SANCT-2026-001' && record.matchStatus !== 'cleared_false_positive' && record.matchStatus !== 'blocked' ) { <button ui-button variant="ghost" size="sm" class="text-success hover:bg-success/10 hover:text-success h-8 cursor-pointer gap-1 text-xs" (click)="handleClearFalsePositive(record)" > <lucide-icon [img]="Check" class="size-3.5" /> <span>Clear False Positive</span> </button> <button ui-button variant="destructive" size="sm" class="h-8 cursor-pointer gap-1 text-xs" (click)="handleBlockEntity(record)" > <lucide-icon [img]="Ban" class="size-3.5" /> <span>Block & Freeze</span> </button> } @if (record.id === 'SANCT-2026-004' && record.matchStatus !== 'blocked') { <button ui-button variant="destructive" size="sm" class="h-8 cursor-pointer gap-1 text-xs" (click)="handleBlockEntity(record)" > <lucide-icon [img]="Ban" class="size-3.5" /> <span>Block Entity</span> </button> } </div> </td> </tr> } @if (filteredRecords().length === 0) { <tr ui-table-row> <td ui-table-cell colspan="6" class="text-muted-foreground py-10 text-center"> <div class="flex flex-col items-center justify-center gap-2"> <lucide-icon [img]="FileSearch" class="text-muted-foreground/50 size-8" /> <p class="text-sm font-medium">No matching screening records found</p> <p class="text-xs">Adjust your search keyword, country filter, or match threshold.</p> <button ui-button variant="outline" size="sm" class="mt-2 text-xs" (click)="handleResetFilters()"> Reset Filters </button> </div> </td> </tr> } </tbody> </table> </div> </div> <!-- Sanction Match Investigation Modal / Dialog --> <ui-sanctions-investigation-dialog [open]="isInvestigationOpen()" (openChange)="isInvestigationOpen.set($event)" [selectedRecord]="selectedRecord()" [thresholdValue]="currentThreshold()" (clear)="handleClearFalsePositive($event)" (escalate)="handleEscalate($event)" (block)="handleBlockEntity($event)" /> </div> `, }) export class UiComplianceSanctionsScreenerComponent { protected readonly AlertTriangle = AlertTriangle protected readonly Ban = Ban protected readonly Building2 = Building2 protected readonly Check = Check protected readonly CheckCircle2 = CheckCircle2 protected readonly Eye = Eye protected readonly FileSearch = FileSearch protected readonly Globe = Globe protected readonly Info = Info protected readonly RefreshCw = RefreshCw protected readonly RotateCcw = RotateCcw protected readonly Search = Search protected readonly ShieldAlert = ShieldAlert protected readonly ShieldCheck = ShieldCheck protected readonly ShieldX = ShieldX protected readonly Sliders = Sliders protected readonly User = User protected readonly Users = Users protected readonly X = X @Input() set initialSearch(v: string | undefined) { if (v !== undefined) this.searchQuery.set(v) } @Input() set initialCountry(v: string | undefined) { if (v !== undefined) this.selectedCountry.set(v) } @Input() set initialThreshold(v: number | undefined) { if (v !== undefined) this.thresholdValue.set([v]) } @Input() set initialRecords(v: SanctionRecord[] | undefined) { if (v) this.records.set(cloneRecords(v)) } @Input('class') className?: string readonly searchQuery = signal('Viktor Ivanov') readonly selectedCountry = signal('all') readonly selectedEntityType = signal<'all' | EntityType>('all') readonly thresholdValue = signal<number[]>([85]) readonly records = signal<SanctionRecord[]>(cloneRecords(defaultSanctionRecords)) readonly isBatchScreening = signal(false) readonly notificationBanner = signal<{ message: string type: 'success' | 'warning' | 'info' | 'destructive' } | null>(null) readonly selectedRecord = signal<SanctionRecord | null>(null) readonly isInvestigationOpen = signal(false) private bannerTimer: ReturnType<typeof setTimeout> | undefined private batchTimer: ReturnType<typeof setTimeout> | undefined readonly totalScreenedCount = 1420 readonly currentThreshold = computed(() => this.thresholdValue()[0] ?? 85) readonly filteredRecords = computed(() => { const q = this.searchQuery().toLowerCase().trim() const country = this.selectedCountry() const entityType = this.selectedEntityType() return this.records().filter((record) => { if (q) { const matchesName = record.entityName.toLowerCase().includes(q) const matchesAlias = record.aliases.some((a) => a.toLowerCase().includes(q)) const matchesList = record.sanctionList.toLowerCase().includes(q) const matchesId = record.nationalIdOrLei.toLowerCase().includes(q) const matchesSummary = record.matchedAttributesSummary.toLowerCase().includes(q) if (!matchesName && !matchesAlias && !matchesList && !matchesId && !matchesSummary) return false } if (country !== 'all' && record.country !== country) return false if (entityType !== 'all' && record.entityType !== entityType) return false return true }) }) readonly clearedCount = computed(() => { const baseCleared = 1408 const additionalCleared = this.records().filter((r) => r.matchStatus === 'cleared_false_positive').length const newlyBlocked = this.records().filter((r) => r.matchStatus === 'blocked').length return baseCleared + additionalCleared - newlyBlocked }) readonly flaggedCount = computed(() => { const basePending = 12 const cleared = this.records().filter((r) => r.matchStatus === 'cleared_false_positive').length const blocked = this.records().filter((r) => r.matchStatus === 'blocked').length return Math.max(0, basePending - cleared - blocked) }) readonly blockedCount = computed(() => this.records().filter((r) => r.matchStatus === 'blocked').length) get rootClass(): string { return cn('text-foreground w-full space-y-6', this.className) } get batchIconClass(): string { return cn('size-4', this.isBatchScreening() && 'animate-spin') } bannerClass(type: string): string { return cn( 'flex items-center justify-between gap-3 rounded-lg border px-4 py-3 text-xs transition-colors duration-200 sm:text-sm', type === 'success' && 'border-success/30 bg-success/10 text-success', type === 'warning' && 'border-warning/30 bg-warning/10 text-warning', type === 'destructive' && 'bg-destructive/10 border-destructive/30 text-destructive', type === 'info' && 'bg-primary/10 border-primary/20 text-primary', ) } entityPillClass(value: 'all' | EntityType): string { return cn( 'flex-1 rounded px-2 py-1 text-xs font-medium transition-colors', this.selectedEntityType() === value ? 'bg-background text-foreground shadow-xs' : 'text-muted-foreground hover:text-foreground', ) } rowClass(status: string): string { return cn( 'hover:bg-muted/30 transition-colors', status === 'potential_match' && 'bg-warning/[0.02]', status === 'prohibited_match' && 'bg-destructive/[0.03]', status === 'blocked' && 'bg-destructive/[0.06] opacity-90', ) } avatarClass(entityType: EntityType): string { return cn( 'mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-lg text-xs font-bold', entityType === 'individual' ? 'bg-primary/10 text-primary' : 'bg-secondary text-secondary-foreground', ) } aliasPreview(record: SanctionRecord): string { const shown = record.aliases.slice(0, 2).join(', ') return record.aliases.length > 2 ? `${shown} (+${record.aliases.length - 2} more)` : shown } onThresholdChange(value: number[] | number): void { this.thresholdValue.set(Array.isArray(value) ? value : [value]) } openInvestigation(record: SanctionRecord): void { this.selectedRecord.set(record) this.isInvestigationOpen.set(true) } showNotification(message: string, type: 'success' | 'warning' | 'info' | 'destructive' = 'info'): void { this.notificationBanner.set({ message, type }) clearTimeout(this.bannerTimer) this.bannerTimer = setTimeout(() => { if (this.notificationBanner()?.message === message) this.notificationBanner.set(null) }, 4500) } handleRunBatchScreening(): void { this.isBatchScreening.set(true) clearTimeout(this.batchTimer) this.batchTimer = setTimeout(() => { this.isBatchScreening.set(false) this.showNotification( 'Batch screening completed. 1,420 entities cross-referenced against 4 global registries. 12 matches flagged.', 'success', ) }, 1200) } private patchRecord(record: SanctionRecord, patch: Partial<SanctionRecord>): void { this.records.update((prev) => prev.map((r) => (r.id === record.id ? { ...r, ...patch } : r))) const selected = this.selectedRecord() if (selected && selected.id === record.id) this.selectedRecord.set({ ...selected, ...patch }) } handleClearFalsePositive(record: SanctionRecord): void { this.patchRecord(record, { matchStatus: 'cleared_false_positive' }) this.showNotification( `False positive cleared for "${record.entityName}". Compliance audit log logged with officer signature.`, 'success', ) } handleBlockEntity(record: SanctionRecord): void { this.patchRecord(record, { matchStatus: 'blocked' }) this.showNotification( `Entity "${record.entityName}" confirmed PROHIBITED. Asset freeze locked and SAR report staged for FinCEN / EU MLRO.`, 'destructive', ) } handleEscalate(record: SanctionRecord): void { this.showNotification( `Case for "${record.entityName}" escalated to Senior Compliance Officer & Legal Counsel for review.`, 'warning', ) this.isInvestigationOpen.set(false) } handleResetFilters(): void { this.searchQuery.set('') this.selectedCountry.set('all') this.selectedEntityType.set('all') this.thresholdValue.set([85]) this.showNotification('Filters reset to default workbench view.', 'info') } }src/app/components/blocks/compliance-sanctions-screener/sanctions-investigation-dialog.component.ts12.4 kB
// Sanction match investigation & attribute comparison dialog: entity overview banner, // attribute-by-attribute verification matrix, and the 31 CFR Part 587 compliance notice. // Port of the React SanctionsInvestigationDialog 1:1. import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core' import { Ban, Check, Layers, LucideAngularModule, ShieldAlert, ShieldCheck } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiDialogCloseComponent, UiDialogComponent, UiDialogContentComponent, UiDialogDescriptionComponent, UiDialogFooterComponent, UiDialogHeaderComponent, UiDialogTitleComponent, } from '@/ui/dialog/dialog.component' import { UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, } from '@/ui/table/table.component' import type { SanctionRecord } from './compliance-sanctions-types' @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-sanctions-investigation-dialog, [ui-sanctions-investigation-dialog]', standalone: true, host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiDialogCloseComponent, UiDialogComponent, UiDialogContentComponent, UiDialogDescriptionComponent, UiDialogFooterComponent, UiDialogHeaderComponent, UiDialogTitleComponent, UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, ], template: ` <ui-dialog [open]="open" (openChange)="openChange.emit($event)"> @if (selectedRecord; as record) { <ui-dialog-content class="max-h-[90vh] overflow-y-auto sm:max-w-3xl"> <div ui-dialog-header> <div class="flex min-w-0 items-center gap-2.5"> <div [class]="iconWrapClass(record.matchScore)"> @if (record.matchScore >= 85) { <lucide-icon [img]="ShieldAlert" class="size-5" /> } @else { <lucide-icon [img]="ShieldCheck" class="size-5" /> } </div> <div> <h2 ui-dialog-title class="text-base font-bold sm:text-lg"> Sanction Match Investigation & Attribute Comparison </h2> <p ui-dialog-description class="text-muted-foreground mt-0.5 text-xs"> Detailed biometric, legal entity registry, and designated watchlist reconciliation audit trail. </p> </div> </div> </div> <div class="space-y-4 py-2"> <!-- Entity Overview Banner --> <div class="border-border bg-muted/40 grid grid-cols-1 gap-3 rounded-lg border p-3 text-xs sm:grid-cols-3"> <div> <span class="text-muted-foreground block">Subject Entity Name</span> <strong class="text-foreground text-sm font-semibold">{{ record.entityName }}</strong> @if (record.originalScript) { <div class="text-muted-foreground mt-0.5 text-xs"> {{ record.originalScript }} </div> } </div> <div> <span class="text-muted-foreground block">Fuzzy Algorithm Score</span> <div class="mt-0.5 flex items-center gap-1.5"> <span class="text-sm font-bold tabular-nums" [class]="record.matchScore >= 85 ? 'text-warning' : 'text-success'" > {{ record.matchScore }}% Match </span> <span ui-badge [variant]="scoreBadgeVariant(record.matchScore)" class="py-0 text-xs whitespace-normal" > {{ scoreBadgeLabel(record.matchScore) }} </span> </div> <span class="text-muted-foreground text-xs">Threshold: {{ thresholdValue }}%</span> </div> <div> <span class="text-muted-foreground block">Primary Watchlist Source</span> <strong class="text-foreground mt-0.5 line-clamp-1 font-medium">{{ record.sanctionList }}</strong> <span class="text-muted-foreground text-xs">{{ record.jurisdiction }}</span> </div> </div> <!-- Attribute Comparison Matrix --> <div class="space-y-2"> <h4 class="text-muted-foreground flex items-center gap-1.5 text-xs font-semibold tracking-wider uppercase" > <lucide-icon [img]="Layers" class="text-primary size-3.5" /> <span>Attribute-by-Attribute Verification Matrix</span> </h4> <div class="border-border overflow-hidden rounded-lg border"> <div class="overflow-x-auto"> <table ui-table> <thead ui-table-header class="bg-muted/60"> <tr ui-table-row> <th ui-table-head class="w-[140px] text-xs">Attribute</th> <th ui-table-head class="min-w-[160px] text-xs">Submitted Query Data</th> <th ui-table-head class="min-w-[200px] text-xs">Designated Watchlist Record</th> <th ui-table-head class="w-[130px] text-right text-xs">Match Verdict</th> </tr> </thead> <tbody ui-table-body> @for (attr of record.attributeComparisons; track attr.field) { <tr ui-table-row class="hover:bg-muted/20 text-xs"> <td ui-table-cell class="text-foreground py-2.5 align-top font-medium"> {{ attr.field }} </td> <td ui-table-cell class="text-muted-foreground py-2.5 align-top"> <span class="text-foreground font-medium">{{ attr.submittedValue }}</span> </td> <td ui-table-cell class="py-2.5 align-top"> <div class="space-y-0.5"> <span class="text-foreground font-medium">{{ attr.watchlistValue }}</span> @if (attr.note) { <p class="text-muted-foreground/80 text-xs italic"> {{ attr.note }} </p> } </div> </td> <td ui-table-cell class="py-2.5 text-right align-top"> @if (attr.matchType === 'exact') { <span ui-badge variant="destructive" class="text-xs font-semibold whitespace-normal tabular-nums" > {{ attr.score }}% Exact Match </span> } @else if (attr.matchType === 'fuzzy' || attr.matchType === 'partial') { <span ui-badge variant="outline" class="border-warning/40 bg-warning/15 text-warning text-xs font-semibold whitespace-normal tabular-nums" > {{ attr.score }}% Fuzzy Hit </span> } @else { <span ui-badge variant="outline" class="border-success/30 bg-success/10 text-success text-xs font-semibold" > Clear </span> } </td> </tr> } </tbody> </table> </div> </div> </div> <!-- Legal Authority & Compliance Advisory Warning --> @if (record.matchScore >= 85) { <div class="border-destructive/30 bg-destructive/5 text-foreground space-y-2 rounded-lg border p-3.5 text-xs" > <div class="text-destructive flex min-w-0 items-center gap-2 font-semibold"> <lucide-icon [img]="ShieldAlert" class="size-4" /> <span>Mandatory Sanctions Compliance & Asset Freeze Notice (31 CFR Part 587)</span> </div> <p class="text-muted-foreground"> If confirmed prohibited, all property and interests in property of this entity within U.S. or EU jurisdiction must be immediately blocked and reported to OFAC / designated national sanctions authorities within 10 business days. </p> <div class="text-muted-foreground flex flex-wrap items-center gap-3 font-mono text-xs"> <span>Program: {{ record.program }}</span> <span>•</span> <span>SAR Filing: Required within 30d</span> </div> </div> } </div> <div ui-dialog-footer class="border-border flex flex-col-reverse gap-2 border-t pt-3 sm:flex-row sm:items-center sm:justify-between" > <button ui-dialog-close ui-button variant="outline" size="sm" class="text-xs">Dismiss / Close</button> <div class="flex min-w-0 items-center gap-2"> @if (record.matchStatus !== 'cleared_false_positive') { <button ui-button variant="outline" size="sm" class="text-success hover:bg-success/10 hover:text-success gap-1.5 text-xs" (click)="handleClear(record)" > <lucide-icon [img]="Check" class="size-3.5" /> <span>Clear False Positive</span> </button> } <button ui-button variant="secondary" size="sm" class="gap-1.5 text-xs" (click)="escalate.emit(record)"> <lucide-icon [img]="ShieldAlert" class="size-3.5" /> <span>Escalate to MLRO</span> </button> @if (record.matchStatus !== 'blocked') { <button ui-button variant="destructive" size="sm" class="gap-1.5 text-xs" (click)="handleBlock(record)"> <lucide-icon [img]="Ban" class="size-3.5" /> <span>Confirm Prohibited & Block</span> </button> } </div> </div> </ui-dialog-content> } </ui-dialog> `, }) export class UiSanctionsInvestigationDialogComponent { protected readonly Ban = Ban protected readonly Check = Check protected readonly Layers = Layers protected readonly ShieldAlert = ShieldAlert protected readonly ShieldCheck = ShieldCheck @Input() open = false @Output() openChange = new EventEmitter<boolean>() @Input() selectedRecord: SanctionRecord | null = null @Input() thresholdValue = 85 @Output() clear = new EventEmitter<SanctionRecord>() @Output() escalate = new EventEmitter<SanctionRecord>() @Output() block = new EventEmitter<SanctionRecord>() iconWrapClass(score: number): string { return cn( 'flex size-9 shrink-0 items-center justify-center rounded-lg', score >= 85 ? 'bg-warning/15 text-warning' : 'bg-success/15 text-success', ) } scoreBadgeVariant(score: number): 'destructive' | 'outline' | 'success' { if (score >= 95) return 'destructive' if (score >= 85) return 'outline' return 'success' } scoreBadgeLabel(score: number): string { if (score >= 95) return 'Critical Risk' if (score >= 85) return 'High Confidence' return 'Clear / Clean' } handleClear(record: SanctionRecord): void { this.clear.emit(record) this.openChange.emit(false) } handleBlock(record: SanctionRecord): void { this.block.emit(record) this.openChange.emit(false) } }src/app/components/blocks/compliance-sanctions-screener/compliance-sanctions-types.ts1 kB
export type MatchStatus = 'potential_match' | 'prohibited_match' | 'clear' | 'cleared_false_positive' | 'blocked' export type EntityType = 'individual' | 'corporate' export interface SanctionAttributeComparison { field: string submittedValue: string watchlistValue: string matchType: 'exact' | 'fuzzy' | 'partial' | 'clean' | 'unmatched' score: number note?: string } export interface SanctionRecord { id: string entityName: string originalScript?: string entityType: EntityType matchScore: number matchStatus: MatchStatus sanctionList: string program: string matchedAttributesSummary: string country: string jurisdiction: string dateOfBirthOrIncorporation: string nationalIdOrLei: string address: string aliases: string[] pepLevel?: string remarks?: string lastScreened: string attributeComparisons: SanctionAttributeComparison[] } export interface NotificationBanner { message: string type: 'success' | 'warning' | 'destructive' | 'info' }src/app/components/blocks/compliance-sanctions-screener/compliance-sanctions-data.ts10.5 kB
import type { SanctionRecord } from './compliance-sanctions-types' export const defaultSanctionRecords: SanctionRecord[] = [ { id: 'SANCT-2026-001', entityName: 'Viktor Ivanov', originalScript: 'Иванов Виктор Петрович', entityType: 'individual', matchScore: 94, matchStatus: 'potential_match', sanctionList: 'OFAC SDN List · Sectoral Sanctions', program: 'UKRAINE-EO13662 · Executive Order 14024', matchedAttributesSummary: 'DOB 1974, Russia, Passport 77-04-889102', country: 'Russia', jurisdiction: 'US (OFAC) / EU / UK', dateOfBirthOrIncorporation: '1974-05-12', nationalIdOrLei: '77-04-889102 (RUS)', address: 'Presnenskaya Naberezhnaya 12, Moscow, Russia', aliases: ['Victor Ivanov', 'V. P. Ivanov', 'Viktor Petrovich Ivanov'], pepLevel: 'PEP Tier 2 · Former Deputy Minister of Industry', remarks: 'Designated under Executive Order 14024 for operating in the financial and defense sector.', lastScreened: '10m ago', attributeComparisons: [ { field: 'Full Legal Name', submittedValue: 'Viktor Ivanov', watchlistValue: 'IVANOV, Viktor Petrovich (Иванов Виктор Петрович)', matchType: 'fuzzy', score: 96, note: 'Transliteration and patronymic expansion matched', }, { field: 'Known Aliases & AKAs', submittedValue: 'Victor Ivanov, V. P. Ivanov', watchlistValue: 'Viktor Ivanov, Victor Petrovich Ivanov, Victor Ivanov-Karpov', matchType: 'partial', score: 94, note: 'Direct alias hit in OFAC Specially Designated database', }, { field: 'Date of Birth / Inception', submittedValue: '1974-05-12', watchlistValue: '1974-05-12 (Moscow, USSR / Russian Federation)', matchType: 'exact', score: 100, note: '100% exact cryptographic DOB date match', }, { field: 'Passport / National ID', submittedValue: '77-04-889102 (Russian Federation)', watchlistValue: '77-04-889102 / Int. Passport 51-09-112344', matchType: 'exact', score: 100, note: 'Official primary national identity document match', }, { field: 'Registered Address', submittedValue: 'Presnenskaya Naberezhnaya 12, Moscow', watchlistValue: 'Presnenskaya Embankment 12, Fl. 4, Moscow 123112, Russia', matchType: 'fuzzy', score: 92, note: 'Street and district normalization matched', }, { field: 'PEP Status & Legal Program', submittedValue: 'Executive Director (Disclosed Non-PEP)', watchlistValue: 'OFAC SDN [UKRAINE-EO13662] · PEP Tier 2 · Mandatory Asset Freeze', matchType: 'exact', score: 100, note: 'Undeclared PEP Tier 2 designation active on UK HMT / EU Sanctions', }, ], }, { id: 'SANCT-2026-002', entityName: 'Sarah Connor', originalScript: 'Sarah Connor', entityType: 'individual', matchScore: 12, matchStatus: 'clear', sanctionList: 'No Sanctions Matches (All Databases Checked)', program: 'None · Standard Retail Profile', matchedAttributesSummary: 'No sanction matches · Low risk profile', country: 'United States', jurisdiction: 'Global / FATF Compliant', dateOfBirthOrIncorporation: '1985-02-28', nationalIdOrLei: 'SSN ***-**-4910', address: '742 Evergreen Terrace, Los Angeles, CA 90001, USA', aliases: ['Sarah J. Connor'], pepLevel: 'Non-PEP', remarks: 'Clear of all OFAC, EU, UN, and PEP lists. Auto-approved by automated compliance pipeline.', lastScreened: '18m ago', attributeComparisons: [ { field: 'Full Legal Name', submittedValue: 'Sarah Connor', watchlistValue: 'CONNOR, Sara Helena (Low Soundex Similarity)', matchType: 'fuzzy', score: 18, note: 'Distant phonetical overlap below decision threshold', }, { field: 'Known Aliases & AKAs', submittedValue: 'Sarah J. Connor', watchlistValue: 'No matching designated aliases found', matchType: 'clean', score: 0, note: 'Zero hits across all official alias directories', }, { field: 'Date of Birth / Inception', submittedValue: '1985-02-28', watchlistValue: '1962-11-04 (Bogota, Colombia)', matchType: 'unmatched', score: 0, note: 'DOB discrepancy (>22 years divergence)', }, { field: 'Passport / National ID', submittedValue: 'SSN ***-**-4910 (USA)', watchlistValue: 'COL-9902148 (Republic of Colombia)', matchType: 'unmatched', score: 0, note: 'Different country of issuance and identifier structure', }, { field: 'Registered Address', submittedValue: '742 Evergreen Terrace, Los Angeles, CA, USA', watchlistValue: 'Calle 72 #10-34, Bogota, Colombia', matchType: 'unmatched', score: 0, note: 'Zero geographic intersection', }, { field: 'PEP Status & Legal Program', submittedValue: 'Individual Retail Client', watchlistValue: 'Clean / Unrestricted Jurisdiction', matchType: 'clean', score: 0, note: 'No adverse media or PEP exposure', }, ], }, { id: 'SANCT-2026-003', entityName: 'Elena Rostova', originalScript: 'Elena Rostova-Dimitriou', entityType: 'individual', matchScore: 0, matchStatus: 'clear', sanctionList: 'No Sanctions Matches (All Databases Checked)', program: 'None · Private Banking Profile', matchedAttributesSummary: 'No sanction matches · High assurance clean record', country: 'Cyprus', jurisdiction: 'EU / Global Compliant', dateOfBirthOrIncorporation: '1991-09-17', nationalIdOrLei: 'CY-ID 9920194', address: 'Archbishop Makarios III Ave 45, Limassol 3025, Cyprus', aliases: ['Elena Rostova-Dimitriou'], pepLevel: 'Non-PEP', remarks: 'Verified clean record across UN Consolidated, OFAC SDN, UK HMT, and Interpol Red Notices.', lastScreened: '32m ago', attributeComparisons: [ { field: 'Full Legal Name', submittedValue: 'Elena Rostova', watchlistValue: 'No target record matches identified in any watchlist', matchType: 'clean', score: 0, note: 'Zero hits across 42 global sanction jurisdictions', }, { field: 'Known Aliases & AKAs', submittedValue: 'Elena Rostova-Dimitriou', watchlistValue: 'No target record matches', matchType: 'clean', score: 0, note: 'Zero alias records found', }, { field: 'Date of Birth / Inception', submittedValue: '1991-09-17', watchlistValue: 'No target record matches', matchType: 'clean', score: 0, note: 'Clean verification result', }, { field: 'Passport / National ID', submittedValue: 'CY-ID 9920194 (Cyprus / EU)', watchlistValue: 'No target record matches', matchType: 'clean', score: 0, note: 'Clean verification result', }, { field: 'Registered Address', submittedValue: 'Archbishop Makarios III Ave 45, Limassol, Cyprus', watchlistValue: 'No target record matches', matchType: 'clean', score: 0, note: 'Verified standard EU residential registry', }, { field: 'PEP Status & Legal Program', submittedValue: 'Private Banking Client', watchlistValue: 'Verified Non-PEP · Zero Adverse Media Flag', matchType: 'clean', score: 0, note: 'Clean compliance status', }, ], }, { id: 'SANCT-2026-004', entityName: 'Sberbank Trade Corp', originalScript: 'ПАО Сбербанк / Sberbank Trade Corp', entityType: 'corporate', matchScore: 98, matchStatus: 'prohibited_match', sanctionList: 'EU Sanctions Article 5', program: 'EU Reg 833/2014 Annex XIII · UK Sanctions S.I. 2022/194 · OFAC SSI Directives', matchedAttributesSummary: 'LEI RU-9912048, Sector: Financials, Subsidiary of Sberbank PJSC', country: 'Russia', jurisdiction: 'EU / UK / OFAC Sectoral', dateOfBirthOrIncorporation: '2008-11-14', nationalIdOrLei: 'LEI: 253400X89892182 / INN: 7707083893', address: 'Vavilova St 19, Moscow, 117997, Russia', aliases: ['Sber Trade LLC', 'Sberbank Trading House', 'PJSC Sberbank Sub-Entity'], pepLevel: 'State-Owned Enterprise (SOE) · Comprehensive Asset Freeze', remarks: 'Subject to broad asset freeze and prohibitions on financial dealings, capital markets, and correspondent accounts.', lastScreened: '1h ago', attributeComparisons: [ { field: 'Legal Entity Name', submittedValue: 'Sberbank Trade Corp', watchlistValue: 'PJSC SBERBANK / Sberbank Trade Corp (ПАО Сбербанк)', matchType: 'exact', score: 99, note: 'Substantial majority-owned direct subsidiary matching designated parent', }, { field: 'Known Aliases & AKAs', submittedValue: 'Sber Trade LLC, Sberbank Trading House', watchlistValue: 'Sberbank Trading House / Sber Trade / Sberbank Capital LLC', matchType: 'exact', score: 100, note: '100% match on registered trading names in EU Annex XIII', }, { field: 'Date of Incorporation', submittedValue: '2008-11-14', watchlistValue: '2008-11-14 (Moscow, Russian Federation)', matchType: 'exact', score: 100, note: 'Registration date matching corporate charter record', }, { field: 'LEI / Corporate Tax ID', submittedValue: 'LEI 253400X89892182 / INN 7707083893', watchlistValue: 'LEI 253400X89892182 / INN 7707083893 / OGRN 1027700132195', matchType: 'exact', score: 100, note: 'Unique global Legal Entity Identifier (LEI) direct match', }, { field: 'Registered Head Office', submittedValue: 'Vavilova St 19, Moscow, 117997, Russia', watchlistValue: '19 Vavilova St, Moscow 117997, Russian Federation', matchType: 'exact', score: 98, note: 'Exact match with sanctioned financial headquarters address', }, { field: 'Sanction Program & Enforcement', submittedValue: 'Corporate Investment Account', watchlistValue: 'EU Article 5 / UK HMT Sanctions / OFAC 50% Rule Asset Freeze', matchType: 'exact', score: 100, note: 'Mandatory transaction blocking and immediate regulatory freeze required', }, ], }, ]src/app/components/blocks/compliance-sanctions-screener/index.ts0.4 kB
export { UiComplianceSanctionsScreenerComponent, type EntityType, type SanctionRecord, } from './compliance-sanctions-screener.component' export { UiSanctionsInvestigationDialogComponent } from './sanctions-investigation-dialog.component' export type { MatchStatus, NotificationBanner, SanctionAttributeComparison } from './compliance-sanctions-types' export { defaultSanctionRecords } from './compliance-sanctions-data'
Raw manifest:https://uipkge.dev/r/angular/compliance-sanctions-screener.json