diff --git a/docs/test-architecture.md b/docs/test-architecture.md
index e42294555..537838eb1 100644
--- a/docs/test-architecture.md
+++ b/docs/test-architecture.md
@@ -137,11 +137,16 @@ run does not prove for each one; the taxonomy and cross-repository entries live
fixtures render. It does not prove that the live promo or relations API returns those
payloads, that login or token verification works, or that create/deactivate succeed against
the server.
-- **The RealUnit compliance visual spec answers the customer list and dossier itself.**
- `e2e/realunit-compliance.spec.ts` fulfils `GET /v1/realunit/compliance/customers` and
- `GET /v1/realunit/compliance/customers/:id` with synthetic fixtures (including `addresses`).
- A green run proves those fixtures render. It does not prove that the API returns that payload
- or that the server filters to RealUnit wallets.
+- **The RealUnit compliance visual spec answers the customer list, Dilisense actions, and dossier itself.**
+ `e2e/realunit-compliance.spec.ts` fulfils `GET /v1/realunit/compliance/customers`,
+ `GET /v1/realunit/compliance/customers/:id`, `GET /v1/realunit/compliance/name-check`,
+ `POST /v1/realunit/compliance/name-check` and `POST /v1/realunit/compliance/customers/:id/name-check`
+ with synthetic fixtures (including `addresses` and name-check results). Auth is a synthetic Admin
+ JWT plus staff bootstrap GETs. Unmatched `GET /v1/**` calls return `[]` and other unmatched
+ `/v1/**` methods return `{}`, so a green visual run does not prove which other staff endpoints the
+ screen calls. A green run proves those fixtures render, including the Screen / Screen-all confirm
+ dialogs and a running-batch state. It does not prove that the API returns that payload, that login
+ works, or that the server filters to RealUnit wallets.
- **Two specs force KYC completeness.** Both collection-invoice cases — the refused QR and the
stored-detail error — override `**/v2/user` so that `kyc.dataComplete` is read as `true`, because
the invoice button is gated on that value. A green run therefore proves nothing about the gate for
diff --git a/e2e-stack/specs/realunit.spec.ts b/e2e-stack/specs/realunit.spec.ts
index 6f8466bb9..0d6722873 100644
--- a/e2e-stack/specs/realunit.spec.ts
+++ b/e2e-stack/specs/realunit.spec.ts
@@ -247,7 +247,7 @@ test.describe('RealUnit area', () => {
`SELECT id FROM asset WHERE name = 'REALU' AND blockchain = 'Sepolia' AND type = 'Token' ORDER BY id ASC LIMIT 1`,
);
if (realu?.id == null) {
- throw new Error("seedWaitingForPaymentBuyQuote: no loc REALU token on Sepolia in seed data");
+ throw new Error('seedWaitingForPaymentBuyQuote: no loc REALU token on Sepolia in seed data');
}
const uid = `RQ${Date.now().toString(36)}${customer.userId}`.replace(/[^a-zA-Z0-9]/g, '').slice(0, 20);
@@ -290,9 +290,7 @@ test.describe('RealUnit area', () => {
)
.not.toBeNull();
- await expect
- .poll(() => normPath(new URL(page.url()).pathname), { timeout: 15000 })
- .toBe('/realunit/quotes');
+ await expect.poll(() => normPath(new URL(page.url()).pathname), { timeout: 15000 }).toBe('/realunit/quotes');
assertNoErrors(pageErrors, consoleErrors);
});
@@ -331,9 +329,7 @@ test.describe('RealUnit area', () => {
)
.not.toBeNull();
- await expect
- .poll(() => normPath(new URL(page.url()).pathname), { timeout: 15000 })
- .toBe('/realunit/quotes');
+ await expect.poll(() => normPath(new URL(page.url()).pathname), { timeout: 15000 }).toBe('/realunit/quotes');
assertNoErrors(pageErrors, consoleErrors);
});
@@ -475,18 +471,20 @@ test.describe('RealUnit area', () => {
await expect(page.getByPlaceholder('Search by ID, email, phone or name...')).toBeVisible();
await expect(page.getByRole('button', { name: 'Search' })).toBeVisible();
+ // thead
cells map to ARIA role "cell" in this app, not "columnheader".
+ await expect(page.getByText('Last Dilisense check')).toBeVisible();
+ await expect(page.getByRole('button', { name: 'Screen all' })).toBeVisible();
+ await expect(
+ page.getByText('Something went wrong. Please try again. If the issue persists please reach out to our support.'),
+ ).toHaveCount(0);
- // Loading resolves to customers count, empty filter copy, or handled ErrorHint — not a crash.
- // Empty result can show "Customers: 0" and "No entries found" at once; .first() avoids strict mode.
+ // Loading resolves to customers count or empty filter copy — not a crash. GET name-check on
+ // mount must succeed (ErrorHint is not an accepted happy-path settle). Empty result can show
+ // "Customers: 0" and "No entries found" at once; .first() avoids strict mode.
const settled = page
.getByText(/^Customers:/)
.or(page.getByText('No entries found'))
- .or(page.getByText('All accounts are hidden by the filter above'))
- .or(
- page.getByText(
- 'Something went wrong. Please try again. If the issue persists please reach out to our support.',
- ),
- );
+ .or(page.getByText('All accounts are hidden by the filter above'));
await expect(settled.first()).toBeVisible();
assertNoErrors(pageErrors, consoleErrors);
diff --git a/e2e/realunit-compliance.spec.ts b/e2e/realunit-compliance.spec.ts
index 9da526d2f..0abaef6e9 100644
--- a/e2e/realunit-compliance.spec.ts
+++ b/e2e/realunit-compliance.spec.ts
@@ -1,34 +1,41 @@
-import { test, expect, APIRequestContext, Page, Route } from '@playwright/test';
-import * as fs from 'fs';
-import * as path from 'path';
-import { createTestCredentials } from './test-wallet';
+import { test, expect, Page, Route } from '@playwright/test';
/**
* E2E Visual Regression Tests: RealUnit staff Compliance dashboards
*
* Routes:
- * - /realunit/compliance (customer search)
+ * - /realunit/compliance (customer search + Dilisense screen actions)
* - /realunit/compliance/user/:id (reduced dossier — long vertical page)
*
- * Auth is REAL (same admin-token flow as compliance.spec.ts / compliance-recommendation-graph.spec.ts): the api must
- * be reachable for `/v1/auth` and the frontend's own user/role fetch. The admin user has the ADMIN role, which
- * `useRealunitGuard` accepts (ADMIN | REALUNIT).
+ * Auth is a synthetic Admin JWT. Feature data and staff bootstrap GETs are MOCKED, so the suite
+ * does not need a live API. A green run does not prove production auth or that the API returns
+ * these customer fields.
*
* Feature data is MOCKED with synthetic fixtures via page.route(...), so the baselines are deterministic AND contain
- * NO real production data. Only the RealUnit-scoped compliance endpoints are intercepted; everything else
- * (auth/role/user/settings) is passed through via route.continue().
+ * NO real production data.
*
* The search screen has NO URL query support (unlike DFX /compliance): the query lives in a controlled input, so the
* test fills the input and presses Enter (the screen's onKeyDown handler runs handleSearch).
*
* Intercepted endpoints (base `/v1/` is prepended by useApi):
- * - GET realunit/compliance/customers[?key=...] (upfront list / search → RealUnitCustomerListDto[])
- * - GET realunit/compliance/customers/:id (dossier → RealUnitCustomerDetailDto)
+ * - GET realunit/compliance/customers[?key=...] (upfront list / search → RealUnitCustomerListDto[])
+ * - GET realunit/compliance/customers/:id (dossier → RealUnitCustomerDetailDto)
+ * - GET realunit/compliance/name-check (batch status)
+ * - POST realunit/compliance/name-check (start batch)
+ * - POST realunit/compliance/customers/:id/name-check
*
* Synthetic fixtures: fake ids (7100+), fixed ISO dates, fake names/emails/IBANs — no production data.
*/
-const API_URL = process.env.REACT_APP_API_URL! + '/v1';
+function jwt(): string {
+ const encode = (value: object) => Buffer.from(JSON.stringify(value)).toString('base64url');
+ return `${encode({ alg: 'none', typ: 'JWT' })}.${encode({
+ account: 1,
+ user: 1,
+ role: 'Admin',
+ exp: Math.floor(Date.now() / 1000) + 3600,
+ })}.synthetic`;
+}
// Author marker the backend stamps on customer messages (mirrors CustomerAuthor in src/util/support-stats.ts).
const CUSTOMER_AUTHOR = 'Customer';
@@ -36,46 +43,10 @@ const CUSTOMER_AUTHOR = 'Customer';
// Numeric id of the customer whose dossier is screenshotted.
const CUSTOMER_ID = 7101;
-/**
- * Read ADMIN_SEED from the API .env file
- */
-function getAdminSeed(): string {
- const apiEnvPath = path.join(__dirname, '../../api/.env');
- if (!fs.existsSync(apiEnvPath)) {
- throw new Error(`API .env file not found at ${apiEnvPath}. Run 'npm run setup' in the API directory first.`);
- }
- const content = fs.readFileSync(apiEnvPath, 'utf8');
- const match = content.match(/^ADMIN_SEED=(.*)$/m);
- if (!match || !match[1]) {
- throw new Error('ADMIN_SEED not found in API .env file. Run "npm run setup" in the API directory first.');
- }
- return match[1];
-}
-
-/**
- * Authenticate with admin credentials
- */
-async function getAdminAuth(request: APIRequestContext): Promise {
- const adminSeed = getAdminSeed();
- const credentials = await createTestCredentials(adminSeed);
-
- const response = await request.post(`${API_URL}/auth`, {
- data: credentials,
- });
-
- if (!response.ok()) {
- const body = await response.text().catch(() => 'unknown');
- throw new Error(`Admin auth failed: ${response.status()} - ${body}`);
- }
-
- const data = await response.json();
- return data.accessToken;
-}
-
// ---------------------------------------------------------------------------
// Synthetic fixtures (mirror the RealUnit reduced-compliance DTOs from src/dto/realunit-compliance.dto.ts).
-// This is the REDUCED tenant view: NO DFX AML work products (no name-check, no amlCheck/amlReason, no notes,
-// no limitRequest, no recommendation graph).
+// This is the REDUCED tenant view: Dilisense last-check summary fields only — no DFX AML work products
+// (no amlCheck/amlReason, no notes, no limitRequest, no recommendation graph).
// ---------------------------------------------------------------------------
interface RealUnitCustomerListDto {
@@ -87,6 +58,10 @@ interface RealUnitCustomerListDto {
name?: string;
// mirrors the real DTO field since the Balance column was added
balance?: number;
+ lastNameCheckDate?: string;
+ lastNameCheckStatus?: 'NoMatch' | 'MatchWithoutBirthday' | 'MatchWithBirthday';
+ lastNameCheckEvaluation?: 'Confirmed' | 'Ignored' | 'NotMatching' | 'Canceled';
+ canScreen: boolean;
}
// ~4 synthetic search results (one empty account exercises the default hide-empty toggle).
@@ -99,6 +74,9 @@ const SEARCH_RESULTS: RealUnitCustomerListDto[] = [
mail: 'ops@acme-example.com',
name: 'ACME Example AG',
balance: 1250,
+ canScreen: true,
+ lastNameCheckDate: '2024-06-15T12:00:00.000Z',
+ lastNameCheckStatus: 'NoMatch',
},
{
id: 7102,
@@ -108,6 +86,9 @@ const SEARCH_RESULTS: RealUnitCustomerListDto[] = [
mail: 'alice@example.com',
name: 'Alice Muster',
balance: 30.5,
+ canScreen: true,
+ lastNameCheckDate: '2024-03-01T12:00:00.000Z',
+ lastNameCheckStatus: 'MatchWithBirthday',
},
// Bob stays visible despite balance 0 because name/mail are set — documents the filter semantics
{
@@ -118,6 +99,9 @@ const SEARCH_RESULTS: RealUnitCustomerListDto[] = [
mail: 'bob@example.com',
name: 'Bob Beispiel',
balance: 0,
+ canScreen: true,
+ lastNameCheckDate: '2024-01-20T12:00:00.000Z',
+ lastNameCheckStatus: 'MatchWithoutBirthday',
},
// intentionally no name/mail/accountType — the only empty account; hidden by the toggle in the default
// view, shown in search because an active search bypasses the filter
@@ -126,6 +110,7 @@ const SEARCH_RESULTS: RealUnitCustomerListDto[] = [
kycStatus: 'NA',
kycLevel: '0',
balance: 0,
+ canScreen: false,
},
];
@@ -173,12 +158,38 @@ const DOSSIER = {
// Customer-scoped slices (reduced)
kycFiles: [
{ uid: 'file-7101-1', type: 'Identification', name: 'passport.pdf', created: '2024-01-02T00:00:00.000Z' },
- { uid: 'file-7101-2', type: 'AdditionalDocuments', name: 'commercial-register.pdf', created: '2024-01-03T00:00:00.000Z' },
+ {
+ uid: 'file-7101-2',
+ type: 'AdditionalDocuments',
+ name: 'commercial-register.pdf',
+ created: '2024-01-03T00:00:00.000Z',
+ },
],
kycSteps: [
- { id: 7201, name: 'Contract', type: 'Contract', status: 'Completed', sequenceNumber: 1, created: '2024-01-02T00:00:00.000Z' },
- { id: 7202, name: 'Ident', type: 'Auto', status: 'Completed', sequenceNumber: 2, created: '2024-01-03T00:00:00.000Z' },
- { id: 7203, name: 'LegalEntity', type: 'Manual', status: 'InProgress', sequenceNumber: 3, created: '2024-01-04T00:00:00.000Z' },
+ {
+ id: 7201,
+ name: 'Contract',
+ type: 'Contract',
+ status: 'Completed',
+ sequenceNumber: 1,
+ created: '2024-01-02T00:00:00.000Z',
+ },
+ {
+ id: 7202,
+ name: 'Ident',
+ type: 'Auto',
+ status: 'Completed',
+ sequenceNumber: 2,
+ created: '2024-01-03T00:00:00.000Z',
+ },
+ {
+ id: 7203,
+ name: 'LegalEntity',
+ type: 'Manual',
+ status: 'InProgress',
+ sequenceNumber: 3,
+ created: '2024-01-04T00:00:00.000Z',
+ },
],
transactions: [
{
@@ -298,8 +309,16 @@ const DOSSIER = {
information: 'Customer reported a missing incoming transfer; resolved after bank reconciliation.',
transaction: { id: 7301, uid: 'TX-7301', type: 'Buy', sourceType: 'BuyCrypto', amountInChf: 10000 },
messages: [
- { author: CUSTOMER_AUTHOR, message: 'I sent 10000 CHF but do not see the tokens yet.', created: '2024-01-05T08:00:00.000Z' },
- { author: 'Rita Clerk', message: 'We located the payment, the tokens have now been credited.', created: '2024-01-05T11:00:00.000Z' },
+ {
+ author: CUSTOMER_AUTHOR,
+ message: 'I sent 10000 CHF but do not see the tokens yet.',
+ created: '2024-01-05T08:00:00.000Z',
+ },
+ {
+ author: 'Rita Clerk',
+ message: 'We located the payment, the tokens have now been credited.',
+ created: '2024-01-05T11:00:00.000Z',
+ },
],
},
{
@@ -313,48 +332,99 @@ const DOSSIER = {
department: 'Compliance',
information: 'Follow-up on the beneficial ownership declaration for the organization.',
messages: [
- { author: CUSTOMER_AUTHOR, message: 'Please find the updated ownership declaration attached.', created: '2024-01-07T09:30:00.000Z' },
- { author: 'Tom Support', message: 'Thank you, we are reviewing the document.', created: '2024-01-07T14:15:00.000Z' },
+ {
+ author: CUSTOMER_AUTHOR,
+ message: 'Please find the updated ownership declaration attached.',
+ created: '2024-01-07T09:30:00.000Z',
+ },
+ {
+ author: 'Tom Support',
+ message: 'Thank you, we are reviewing the document.',
+ created: '2024-01-07T14:15:00.000Z',
+ },
],
},
],
};
// ---------------------------------------------------------------------------
-// Routing: intercept ONLY the RealUnit compliance endpoints; pass everything else through.
+// Routing: named RealUnit compliance endpoints plus staff bootstrap GETs. Unmatched GET /v1/**
+// returns [] and other unmatched /v1/** methods return {} so a synthetic JWT does not 401.
// The list endpoint is `.../customers?key=...`; the detail endpoint is `.../customers/:id` — match detail first.
// ---------------------------------------------------------------------------
const DETAIL_RE = /\/v1\/realunit\/compliance\/customers\/(\d+)(?:\?|$)/;
const SEARCH_RE = /\/v1\/realunit\/compliance\/customers(?:\?|$)/;
+const NAME_CHECK_BATCH_RE = /\/v1\/realunit\/compliance\/name-check(?:\?|$)/;
+const NAME_CHECK_CUSTOMER_RE = /\/v1\/realunit\/compliance\/customers\/\d+\/name-check(?:\?|$)/;
+
+const IDLE_BATCH = { status: 'Idle', total: 0, done: 0, failed: 0, skipped: 0 };
+const RUNNING_BATCH = { status: 'Running', total: 3, done: 1, failed: 0, skipped: 1, startedBy: 1 };
async function json(route: Route, body: unknown): Promise {
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(body) });
}
-async function installComplianceRoutes(page: Page): Promise {
+/** Compliance mocks plus staff GETs so a synthetic JWT does not 401-clear the session. */
+async function installComplianceRoutes(
+ page: Page,
+ batch: { status: string; total: number; done: number; failed: number; skipped: number } = IDLE_BATCH,
+): Promise {
await page.route('**/v1/**', async (route: Route) => {
- const url = route.request().url();
-
+ const request = route.request();
+ const url = request.url();
+ const path = new URL(url).pathname;
+
+ if (NAME_CHECK_CUSTOMER_RE.test(url)) {
+ return json(route, { id: CUSTOMER_ID, riskStatus: 'NoMatch', date: '2024-06-15T12:00:00.000Z' });
+ }
+ if (NAME_CHECK_BATCH_RE.test(url)) return json(route, batch);
if (DETAIL_RE.test(url)) return json(route, DOSSIER);
if (SEARCH_RE.test(url)) return json(route, SEARCH_RESULTS);
- // everything else (auth, role, user, settings) hits the real api
+ if (
+ request.method() === 'GET' &&
+ ['/v1/language', '/v1/fiat', '/v1/asset', '/v1/bankAccount', '/v1/country'].includes(path)
+ ) {
+ return json(route, []);
+ }
+
+ if (request.method() === 'GET' && path === '/v1/setting/infoBanner') {
+ return json(route, null);
+ }
+
+ if (request.method() === 'GET') return json(route, []);
+ await route.fulfill({ status: 200, contentType: 'application/json', body: '{}' });
+ });
+
+ await page.route('**/v2/**', async (route: Route) => {
+ const request = route.request();
+ const path = new URL(request.url()).pathname;
+
+ if (request.method() === 'GET' && path === '/v2/user') {
+ return json(route, {
+ id: 1,
+ activeAddress: {
+ address: '0x0000000000000000000000000000000000000001',
+ wallet: 'DFX',
+ },
+ addresses: [],
+ kyc: { level: 50, status: 'Completed' },
+ language: { id: 1, name: 'English', symbol: 'EN' },
+ });
+ }
+
await route.continue();
});
}
test.describe('RealUnit Compliance dashboards - Visual Regression Tests', () => {
- let token: string;
-
- test.beforeAll(async ({ request }) => {
- token = await getAdminAuth(request);
- });
+ const token = jwt();
test('search screen renders customer results', async ({ page }) => {
await installComplianceRoutes(page);
- await page.goto(`/realunit/compliance?session=${token}`);
+ await page.goto(`/realunit/compliance?session=${encodeURIComponent(token)}&lang=en`);
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
@@ -381,6 +451,12 @@ test.describe('RealUnit Compliance dashboards - Visual Regression Tests', () =>
await expect(page.getByText('bob@example.com')).toBeVisible();
// active search bypasses the empty filter — the empty account (id 7104) is visible too
await expect(page.getByText('7104')).toBeVisible();
+ await expect(page.getByRole('columnheader', { name: 'Last Dilisense check' })).toBeVisible();
+ await expect(page.getByRole('columnheader', { name: 'Result' })).toBeVisible();
+ await expect(page.getByText('No match')).toBeVisible();
+ await expect(page.getByText('Match without Birthday')).toBeVisible();
+ await expect(page.getByText('Match with Birthday (Open)')).toBeVisible();
+ await expect(page.getByRole('button', { name: 'Screen all' })).toBeVisible();
await page.waitForTimeout(500);
await expect(page).toHaveScreenshot('realunit-compliance-01-search.png', {
@@ -389,10 +465,65 @@ test.describe('RealUnit Compliance dashboards - Visual Regression Tests', () =>
});
});
+ test('Screen on a named row opens the Dilisense confirm dialog', async ({ page }) => {
+ await installComplianceRoutes(page);
+
+ await page.goto(`/realunit/compliance?session=${encodeURIComponent(token)}&lang=en`);
+ await page.waitForLoadState('networkidle');
+ await page.waitForTimeout(1000);
+
+ await expect(page.getByText('ACME Example AG')).toBeVisible();
+ await page.getByRole('button', { name: 'Screen', exact: true }).first().click();
+ await expect(
+ page.getByText('A Dilisense screening consumes provider quota and costs money – continue?'),
+ ).toBeVisible();
+ await page.waitForTimeout(500);
+
+ await expect(page).toHaveScreenshot('realunit-compliance-03-screen-confirm.png', {
+ fullPage: true,
+ maxDiffPixels: 5000,
+ });
+ });
+
+ test('Screen all opens the Dilisense confirm dialog', async ({ page }) => {
+ await installComplianceRoutes(page);
+
+ await page.goto(`/realunit/compliance?session=${encodeURIComponent(token)}&lang=en`);
+ await page.waitForLoadState('networkidle');
+ await page.waitForTimeout(1000);
+
+ await expect(page.getByRole('button', { name: 'Screen all' })).toBeVisible();
+ await page.getByRole('button', { name: 'Screen all' }).click();
+ await expect(page.getByText('Screening all named shareholders consumes Dilisense quota – continue?')).toBeVisible();
+ await page.waitForTimeout(500);
+
+ await expect(page).toHaveScreenshot('realunit-compliance-04-screen-all-confirm.png', {
+ fullPage: true,
+ maxDiffPixels: 5000,
+ });
+ });
+
+ test('running batch disables Screen all and shows progress', async ({ page }) => {
+ await installComplianceRoutes(page, RUNNING_BATCH);
+
+ await page.goto(`/realunit/compliance?session=${encodeURIComponent(token)}&lang=en`);
+ await page.waitForLoadState('networkidle');
+ await page.waitForTimeout(1000);
+
+ await expect(page.getByRole('button', { name: /Screening 1 \/ 3/ })).toBeDisabled();
+ await expect(page.getByRole('button', { name: 'Screen', exact: true }).first()).toBeDisabled();
+ await page.waitForTimeout(500);
+
+ await expect(page).toHaveScreenshot('realunit-compliance-05-batch-running.png', {
+ fullPage: true,
+ maxDiffPixels: 5000,
+ });
+ });
+
test('dossier screen renders the full reduced customer view', async ({ page }) => {
await installComplianceRoutes(page);
- await page.goto(`/realunit/compliance/user/${CUSTOMER_ID}?session=${token}`);
+ await page.goto(`/realunit/compliance/user/${CUSTOMER_ID}?session=${encodeURIComponent(token)}&lang=en`);
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1500);
diff --git a/e2e/screenshots/baseline/realunit-compliance.spec.ts-realunit-compliance-01-search-chromium-darwin.png b/e2e/screenshots/baseline/realunit-compliance.spec.ts-realunit-compliance-01-search-chromium-darwin.png
index 67e01569e..4ffbfb74d 100644
Binary files a/e2e/screenshots/baseline/realunit-compliance.spec.ts-realunit-compliance-01-search-chromium-darwin.png and b/e2e/screenshots/baseline/realunit-compliance.spec.ts-realunit-compliance-01-search-chromium-darwin.png differ
diff --git a/e2e/screenshots/baseline/realunit-compliance.spec.ts-realunit-compliance-02-dossier-chromium-darwin.png b/e2e/screenshots/baseline/realunit-compliance.spec.ts-realunit-compliance-02-dossier-chromium-darwin.png
index e3eba6e8d..ff712d72f 100644
Binary files a/e2e/screenshots/baseline/realunit-compliance.spec.ts-realunit-compliance-02-dossier-chromium-darwin.png and b/e2e/screenshots/baseline/realunit-compliance.spec.ts-realunit-compliance-02-dossier-chromium-darwin.png differ
diff --git a/e2e/screenshots/baseline/realunit-compliance.spec.ts-realunit-compliance-03-screen-confirm-chromium-darwin.png b/e2e/screenshots/baseline/realunit-compliance.spec.ts-realunit-compliance-03-screen-confirm-chromium-darwin.png
new file mode 100644
index 000000000..9c4fea2f9
Binary files /dev/null and b/e2e/screenshots/baseline/realunit-compliance.spec.ts-realunit-compliance-03-screen-confirm-chromium-darwin.png differ
diff --git a/e2e/screenshots/baseline/realunit-compliance.spec.ts-realunit-compliance-04-screen-all-confirm-chromium-darwin.png b/e2e/screenshots/baseline/realunit-compliance.spec.ts-realunit-compliance-04-screen-all-confirm-chromium-darwin.png
new file mode 100644
index 000000000..19f8d0075
Binary files /dev/null and b/e2e/screenshots/baseline/realunit-compliance.spec.ts-realunit-compliance-04-screen-all-confirm-chromium-darwin.png differ
diff --git a/e2e/screenshots/baseline/realunit-compliance.spec.ts-realunit-compliance-05-batch-running-chromium-darwin.png b/e2e/screenshots/baseline/realunit-compliance.spec.ts-realunit-compliance-05-batch-running-chromium-darwin.png
new file mode 100644
index 000000000..400234a63
Binary files /dev/null and b/e2e/screenshots/baseline/realunit-compliance.spec.ts-realunit-compliance-05-batch-running-chromium-darwin.png differ
diff --git a/scripts/handbook/metadata.json b/scripts/handbook/metadata.json
index 2d614b03d..de14df8c2 100644
--- a/scripts/handbook/metadata.json
+++ b/scripts/handbook/metadata.json
@@ -89,7 +89,7 @@
},
"realunit-compliance": {
"title": "RealUnit Compliance",
- "description": "RealUnit-spezifische Compliance-Suche und Dossier."
+ "description": "RealUnit-Compliance: Kundensuche mit letztem Dilisense-Datum und -Ergebnis, Screen pro Zeile und Screen all (jeweils mit Kosten-Bestätigung), laufender Batch mit Fortschritt, und reduziertes Kundendossier."
},
"realunit-support": {
"title": "RealUnit Support",
diff --git a/src/__tests__/realunit-compliance-screen.test.tsx b/src/__tests__/realunit-compliance-screen.test.tsx
index f6554ab0c..b973fb7bb 100644
--- a/src/__tests__/realunit-compliance-screen.test.tsx
+++ b/src/__tests__/realunit-compliance-screen.test.tsx
@@ -1,13 +1,42 @@
// Component tests for the RealUnit compliance customer list screen: default empty-account filter,
-// toggle, search bypass, and empty-state messages. Heavy transitive deps are mocked so the screen
-// can render under @testing-library/react without the full app shell.
+// toggle, search bypass, empty-state messages, and Dilisense name-check actions. Heavy transitive deps
+// are mocked so the screen can render under @testing-library/react without the full app shell.
jest.mock('@dfx.swiss/react', () => ({}));
jest.mock('@dfx.swiss/react-components', () => ({
SpinnerSize: { SM: 'sm', LG: 'lg' },
StyledLoadingSpinner: () => null,
}));
-jest.mock('src/components/error-hint', () => ({ ErrorHint: () => null }));
+jest.mock('src/components/error-hint', () => ({
+ ErrorHint: ({ message }: { message: string }) =>
))}
@@ -158,6 +337,22 @@ export default function RealunitComplianceScreen(): JSX.Element {
)}
)}
+
+ {
+ if (!isConfirming) setPendingConfirm(undefined);
+ }}
+ />
);
}
diff --git a/src/translations/languages/de.json b/src/translations/languages/de.json
index 5d5f601e0..75e09dbec 100644
--- a/src/translations/languages/de.json
+++ b/src/translations/languages/de.json
@@ -376,6 +376,18 @@
"blocked": "gesperrt",
"Pass only allowed when all errors are phone-related": "Pass nur erlaubt, wenn alle Errors Phone-bezogen sind",
"Hide empty accounts": "Leere Accounts ausblenden",
+ "Last Dilisense check": "Letzter Dilisense-Check",
+ "Result": "Ergebnis",
+ "Screen": "Screenen",
+ "Screen all": "Alle screenen",
+ "Screening {{done}} / {{total}}": "Screening {{done}} / {{total}}",
+ "A Dilisense screening consumes provider quota and costs money – continue?": "Ein Dilisense-Screening verbraucht Provider-Kontingent und kostet Geld – fortfahren?",
+ "Screening all named shareholders consumes Dilisense quota – continue?": "Alle namentlich bekannten Aktionäre screenen verbraucht Dilisense-Kontingent – fortfahren?",
+ "Cannot screen without a name": "Ohne Namen nicht screenbar",
+ "No match": "Kein Treffer",
+ "Match without Birthday": "Treffer ohne Geburtsdatum",
+ "Match with Birthday": "Treffer mit Geburtsdatum",
+ "Open": "offen",
"All accounts are hidden by the filter above": "Alle Accounts sind durch den Filter oben ausgeblendet",
"Addresses": "Adressen",
"No addresses": "Keine Adressen",
diff --git a/src/translations/languages/fr.json b/src/translations/languages/fr.json
index 183cc706c..d564ae853 100644
--- a/src/translations/languages/fr.json
+++ b/src/translations/languages/fr.json
@@ -376,6 +376,18 @@
"blocked": "bloqué",
"Pass only allowed when all errors are phone-related": "Pass autorisé uniquement si toutes les erreurs sont liées au téléphone",
"Hide empty accounts": "Masquer les comptes vides",
+ "Last Dilisense check": "Dernier contrôle Dilisense",
+ "Result": "Résultat",
+ "Screen": "Contrôler",
+ "Screen all": "Tout contrôler",
+ "Screening {{done}} / {{total}}": "Screening {{done}} / {{total}}",
+ "A Dilisense screening consumes provider quota and costs money – continue?": "Un contrôle Dilisense consomme le quota du fournisseur et coûte de l'argent – continuer ?",
+ "Screening all named shareholders consumes Dilisense quota – continue?": "Contrôler tous les actionnaires nommés consomme le quota Dilisense – continuer ?",
+ "Cannot screen without a name": "Impossible sans nom",
+ "No match": "Aucune correspondance",
+ "Match without Birthday": "Correspondance sans date de naissance",
+ "Match with Birthday": "Correspondance avec date de naissance",
+ "Open": "ouvert",
"All accounts are hidden by the filter above": "Tous les comptes sont masqués par le filtre ci-dessus",
"Addresses": "Adresses",
"No addresses": "Aucune adresse",
diff --git a/src/translations/languages/it.json b/src/translations/languages/it.json
index 33b636b88..ff9890e8f 100644
--- a/src/translations/languages/it.json
+++ b/src/translations/languages/it.json
@@ -376,6 +376,18 @@
"blocked": "bloccato",
"Pass only allowed when all errors are phone-related": "Pass consentito solo se tutti gli errori sono relativi al telefono",
"Hide empty accounts": "Nascondi conti vuoti",
+ "Last Dilisense check": "Ultimo controllo Dilisense",
+ "Result": "Risultato",
+ "Screen": "Controlla",
+ "Screen all": "Controlla tutti",
+ "Screening {{done}} / {{total}}": "Screening {{done}} / {{total}}",
+ "A Dilisense screening consumes provider quota and costs money – continue?": "Uno screening Dilisense consuma il contingente del provider e costa — continuare?",
+ "Screening all named shareholders consumes Dilisense quota – continue?": "Controllare tutti gli azionisti nominati consuma il contingente Dilisense — continuare?",
+ "Cannot screen without a name": "Impossibile senza nome",
+ "No match": "Nessuna corrispondenza",
+ "Match without Birthday": "Corrispondenza senza data di nascita",
+ "Match with Birthday": "Corrispondenza con data di nascita",
+ "Open": "aperto",
"All accounts are hidden by the filter above": "Tutti i conti sono nascosti dal filtro sopra",
"Addresses": "Indirizzi",
"No addresses": "Nessun indirizzo",