@dossier-wiki/os-sdk 1.4.0 → 1.4.2-staging.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +331 -9
- package/dist/alert-workflow-types.d.ts +24 -1
- package/dist/alert-workflow-types.d.ts.map +1 -1
- package/dist/service.d.ts +164 -17
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +218 -20
- package/dist/service.js.map +1 -1
- package/dist/types.d.ts +895 -4
- package/dist/types.d.ts.map +1 -1
- package/docs/REFERENCE.md +122 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,6 +10,14 @@ npm install @dossier-wiki/os-sdk
|
|
|
10
10
|
bun add @dossier-wiki/os-sdk
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
To build against the API currently on staging, install the `staging` dist-tag
|
|
14
|
+
instead. It is a prerelease built from the `staging` branch and may change
|
|
15
|
+
without a changelog entry:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
npm install @dossier-wiki/os-sdk@staging
|
|
19
|
+
```
|
|
20
|
+
|
|
13
21
|
## Quick start
|
|
14
22
|
|
|
15
23
|
```ts
|
|
@@ -46,7 +54,7 @@ The service composes resource namespaces — call them off the root client:
|
|
|
46
54
|
| `os.channelEndpoints` | Read and manage POS terminal, page, API client, and other endpoint identities resolved during ingestion |
|
|
47
55
|
| `os.individuals` | Individual customers + nested `identificationDocuments`, `faceImages` |
|
|
48
56
|
| `os.entities` | Entity customers + nested `stakeholders` |
|
|
49
|
-
| `os.customers` | Cross-cutting per-customer: `notes`, `files`, `screening`, `riskRating
|
|
57
|
+
| `os.customers` | Cross-cutting per-customer: `notes`, `files`, `screening`, `kyc` (start, review, PDF report, evidence ZIP), `riskRating` (set an officer override / `clearOverride`), `riskAssessments` (list, submit a questionnaire, `evaluate` the risk matrix now), `expectedActivity`, `devices`, `assignment` (assign / unassign / assignMany) |
|
|
50
58
|
| `os.teams` | Domain teams: list/get/create/update/archive/delete, plus `addMembers` / `removeMembers`. Teams carry catalogue-governed `team.tags`, and `list({ tags })` filters on them |
|
|
51
59
|
| `os.accounts` | Per-customer accounts, including `freeze` / `unfreeze` |
|
|
52
60
|
| `os.counterparties` | Counterparties + nested `riskAssessments`, `files` |
|
|
@@ -62,11 +70,11 @@ The service composes resource namespaces — call them off the root client:
|
|
|
62
70
|
| `os.branches` | The institution's own physical or organizational units — list/get/create/update |
|
|
63
71
|
| `os.exchangeRates` | Push exchange rates |
|
|
64
72
|
| `os.states` | Read and atomically set, increment, or decrement resource state; read immutable change history |
|
|
65
|
-
| `os.alerts` | Raise alerts by hand (`create` — free-form, or against an optional custom alert type; org-scoped `externalRequestId` idempotency), read any alert (`get`), edit markdown `notes` (`updateNotes`), link records (`resources.list/add/remove`), plus `comments` and `attemptEvidence`. Needs the `alerts:read` / `alerts:write` API-key permissions |
|
|
66
|
-
| `os.config` | `rules` (incl. `rules.backtest` — dry-run simulation over historical or synthetic transactions), `constants`, `states`, `alertWorkflows` (one per alert type — `SCREENING`, `RULE_TRIGGER`, `CUSTOM`: list/get/save), `assignmentPolicies` (read-only legacy archive), `products`, `currencies`, `eventDefinitions`, `customAlertTypes` (org-defined alert types with a typed-field schema), `webhooks`, `tagCatalogue`, `sync`, plus org-wide `reset` |
|
|
73
|
+
| `os.alerts` | Raise alerts by hand (`create` — free-form, or against an optional custom alert type; org-scoped `externalRequestId` idempotency), read any alert (`get`, with `isBlocked`), edit markdown `notes` (`updateNotes`), link records (`resources.list/add/remove`), make an alert wait on another (`dependencies.list/add/remove` — see [Blocking alerts](#blocking-alerts)), plus `comments` and `attemptEvidence`. Needs the `alerts:read` / `alerts:write` API-key permissions |
|
|
74
|
+
| `os.config` | `rules` (incl. `rules.backtest` — dry-run simulation over historical or synthetic transactions), `constants`, `states`, `alertWorkflows` (one per alert type — `SCREENING`, `RULE_TRIGGER`, `CUSTOM`: list/get/save), `assignmentPolicies` (read-only legacy archive), `products`, `profileTemplates` (the template a customer is created against: profile fields, risk matrix, KYC workflows, creation-form setup), `currencies`, `eventDefinitions`, `customAlertTypes` (org-defined alert types with a typed-field schema), `webhooks`, `tagCatalogue`, `sync`, plus org-wide `reset` |
|
|
67
75
|
| `os.setup` | Org setup status + initialization |
|
|
68
76
|
| `os.settings` | Sync snapshots + drift |
|
|
69
|
-
| `os.reports` | On-demand PDF reports: `alert`, `customerProfile`, `counterpartyProfile`, `resourceProfile` — each resolves to the PDF bytes (`Uint8Array`)
|
|
77
|
+
| `os.reports` | On-demand PDF reports: `alert`, `customerProfile`, `counterpartyProfile`, `resourceProfile` — each resolves to the PDF bytes (`Uint8Array`). Needs the `reports:read` API-key permission (included in Full Access) |
|
|
70
78
|
| `os.customLists` | Custom screening lists: list/entry CRUD, ≤500-row bulk import (per-row rejection + `externalRef` upsert), enable/disable, archive/unarchive. Enabled LIVE lists are checked by every screening run alongside external providers. |
|
|
71
79
|
|
|
72
80
|
See [docs/REFERENCE.md](./docs/REFERENCE.md) for the full method list and endpoint mapping.
|
|
@@ -373,10 +381,289 @@ Migration:
|
|
|
373
381
|
- Use `status` for the initial customer lifecycle state. New records default to
|
|
374
382
|
`ONBOARDING`; `monitoringStatus` and `riskRating` are not create inputs.
|
|
375
383
|
|
|
384
|
+
## Profile templates & risk matrix
|
|
385
|
+
|
|
386
|
+
A **Profile Template** is to a customer what a **Product** is to an account:
|
|
387
|
+
the template it is created against. It declares the customer's typed
|
|
388
|
+
**profile fields** (`fieldConfig`, the same `TypedFieldDefinition` shape as
|
|
389
|
+
`Product.accountFieldConfig`), a **risk matrix** that computes the customer's
|
|
390
|
+
risk rating from those fields, KYC workflow bindings, and the **creation
|
|
391
|
+
setup** of the simple Add Individual / Add Entity forms. Templates live under
|
|
392
|
+
`os.config.profileTemplates` and need `config:read` / `config:write`.
|
|
393
|
+
|
|
394
|
+
```ts
|
|
395
|
+
// 1. Define a template (once, or via Tenant Sync).
|
|
396
|
+
await os.config.profileTemplates.create({
|
|
397
|
+
key: 'individual-standard', // immutable; connectors use it instead of a UUID
|
|
398
|
+
name: 'Individual — standard onboarding',
|
|
399
|
+
partyType: 'INDIVIDUAL',
|
|
400
|
+
isDefault: true, // new individuals get it when no key is sent
|
|
401
|
+
fieldConfig: [
|
|
402
|
+
{ key: 'pepStatus', displayName: 'PEP status', type: 'SINGLE_SELECT', required: false,
|
|
403
|
+
options: ['No PEP involvement', 'Former PEP', 'Domestic PEP'] },
|
|
404
|
+
{ key: 'onboardingChannel', displayName: 'Onboarding channel', type: 'SINGLE_SELECT', required: false,
|
|
405
|
+
options: ['Face to face', 'Non face to face'] },
|
|
406
|
+
],
|
|
407
|
+
riskMatrix: {
|
|
408
|
+
scale: [{ level: 'LOW', score: 1 }, { level: 'MEDIUM', score: 2 }, { level: 'HIGH', score: 3 }],
|
|
409
|
+
bands: [{ rating: 'LOW', below: 1.67 }, { rating: 'MEDIUM', below: 2.34 }, { rating: 'HIGH' }],
|
|
410
|
+
factors: [
|
|
411
|
+
{ key: 'pepStatus', label: 'PEP status', weight: 0.6, field: 'pepStatus', levels: [
|
|
412
|
+
{ option: 'No PEP involvement', level: 'LOW' },
|
|
413
|
+
{ option: 'Former PEP', level: 'MEDIUM' },
|
|
414
|
+
{ option: 'Domestic PEP', level: 'HIGH' },
|
|
415
|
+
] },
|
|
416
|
+
{ key: 'channel', label: 'Onboarding channel', weight: 0.4, field: 'onboardingChannel', levels: [
|
|
417
|
+
{ option: 'Face to face', level: 'LOW' },
|
|
418
|
+
{ option: 'Non face to face', level: 'HIGH' },
|
|
419
|
+
] },
|
|
420
|
+
],
|
|
421
|
+
escalations: [
|
|
422
|
+
{ label: 'PEP involvement', minimumRating: 'HIGH',
|
|
423
|
+
when: { field: 'factor.pepStatus.level', operator: '=', value: 'HIGH' } },
|
|
424
|
+
],
|
|
425
|
+
outcomes: [
|
|
426
|
+
{ rating: 'LOW', requiredDueDiligence: 'SDD', reviewEveryMonths: 36 },
|
|
427
|
+
{ rating: 'MEDIUM', requiredDueDiligence: 'CDD', reviewEveryMonths: 24 },
|
|
428
|
+
{ rating: 'HIGH', requiredDueDiligence: 'EDD', reviewEveryMonths: 12 },
|
|
429
|
+
],
|
|
430
|
+
},
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
// 2. Create a customer with profile fields (Tier A: validated against the template).
|
|
434
|
+
const customer = await os.individuals.create({
|
|
435
|
+
externalId: 'USR-10044',
|
|
436
|
+
fullName: 'Nadia Perera',
|
|
437
|
+
profileTemplateKey: 'individual-standard', // optional here: it is the default
|
|
438
|
+
fields: [
|
|
439
|
+
{ key: 'pepStatus', type: 'SINGLE_SELECT', value: 'No PEP involvement' },
|
|
440
|
+
{ key: 'onboardingChannel', type: 'SINGLE_SELECT', value: 'Non face to face' },
|
|
441
|
+
],
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// 3. Read the rating. The matrix runs in the background (a few seconds after
|
|
445
|
+
// the last profile write, plus a daily sweep). `evaluate` runs it now.
|
|
446
|
+
const assessment = await os.customers.riskAssessments.evaluate(customer.id);
|
|
447
|
+
assessment.resultingRiskRating; // 'MEDIUM' (0.6 × 1 + 0.4 × 3 = 1.8)
|
|
448
|
+
assessment.compositeScore; // 1.8 — intermediate number, not the rating
|
|
449
|
+
assessment.breakdown?.factors; // input → criterion → level → score → weighted, per factor
|
|
450
|
+
assessment.requiredDueDiligence; // 'CDD'
|
|
451
|
+
assessment.nextReviewAt; // 'YYYY-MM-DD'
|
|
452
|
+
|
|
453
|
+
const latest = await os.individuals.get(customer.id);
|
|
454
|
+
latest.riskRating; // 'MEDIUM'
|
|
455
|
+
latest.riskRatingSource; // 'MATRIX'
|
|
456
|
+
latest.fields; // [{ key: 'pepStatus', type: 'SINGLE_SELECT', value: 'No PEP involvement' }, …]
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
How factors score: each factor has a **weight** (weights sum to 1) and turns
|
|
460
|
+
one profile field into points, which the weight scales into the composite.
|
|
461
|
+
Select fields give each option a level (`levels`), optionally with its own
|
|
462
|
+
`score` — points per selection, overriding the level's scale score while the
|
|
463
|
+
level still drives display and escalations. **Yes/no** (BOOLEAN) fields use
|
|
464
|
+
the options `"true"` and `"false"`. **Number and money** fields use `ranges`
|
|
465
|
+
(`{ min, max, level, score? }`, min inclusive, max exclusive, either end open,
|
|
466
|
+
no overlaps; money compares in major units of the field's currency). A
|
|
467
|
+
**multi-select** scores the selected option with the highest score. Anything
|
|
468
|
+
else uses explicit `criteria` condition trees.
|
|
469
|
+
|
|
470
|
+
```ts
|
|
471
|
+
{ key: 'expectedMonthly', label: 'Expected monthly investment', weight: 0.1,
|
|
472
|
+
field: 'expectedMonthlyInvestment', ranges: [ // a MONETARY field
|
|
473
|
+
{ max: 100_000, level: 'LOW' },
|
|
474
|
+
{ min: 100_000, max: 1_000_000, level: 'MEDIUM' },
|
|
475
|
+
{ min: 1_000_000, level: 'HIGH', score: 4 }, // own points instead of HIGH's scale score
|
|
476
|
+
] }
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
How it behaves:
|
|
480
|
+
|
|
481
|
+
- **Updates merge.** `individuals.update` / `entities.update` accept `fields`:
|
|
482
|
+
listed keys change, `{ key, type, value: null }` clears a field, unlisted
|
|
483
|
+
keys stay. Pass `profileTemplateKey` to switch template (same party type).
|
|
484
|
+
Each profile write re-scores the customer (bursts are coalesced), so a CRM
|
|
485
|
+
pushing its daily delta with `PATCH` gets a fresh rating within seconds.
|
|
486
|
+
- **Officer overrides are sticky.** `customers.riskRating.update` sets
|
|
487
|
+
`riskRatingSource: 'MANUAL'`. Matrix runs still record their computed rating
|
|
488
|
+
(`customers.riskAssessments.list`, `method: 'MATRIX'`) but never overwrite a
|
|
489
|
+
MANUAL rating. `customers.riskRating.clearOverride(customerId)` hands the
|
|
490
|
+
rating back to the matrix and resolves to the latest matrix assessment.
|
|
491
|
+
- **Assessments share one list.** `customers.riskAssessments.list` returns
|
|
492
|
+
questionnaires (`method: 'QUESTIONNAIRE'`) and matrix runs (`method:
|
|
493
|
+
'MATRIX'`, with `compositeScore`, `breakdown`, `profileTemplateVersion`,
|
|
494
|
+
`trigger`, `requiredDueDiligence`, `nextReviewAt`). A matrix run is written
|
|
495
|
+
only when its inputs or outcome change.
|
|
496
|
+
- **Webhooks.** When the matrix changes a rating, `customer.risk_changed` is
|
|
497
|
+
delivered with `source: 'risk_matrix'`, `previousRating`, `newRating`,
|
|
498
|
+
`compositeScore`, `requiredDueDiligence` and `riskAssessmentId`.
|
|
499
|
+
- **Templates are versioned.** Changing `fieldConfig`, `riskMatrix`,
|
|
500
|
+
`kycWorkflows` or the creation setup publishes a new version
|
|
501
|
+
(`profileTemplates.versions(id)`, each with the `changes` it made, or
|
|
502
|
+
`version(id, n)` for one); `fieldConfig` / `riskMatrix` changes also
|
|
503
|
+
re-assess every customer on the template. Customers always follow the
|
|
504
|
+
latest version. `restoreVersion(id, n)` republishes an old version's
|
|
505
|
+
definition as the newest one (re-validated, so a definition naming a
|
|
506
|
+
deleted constant is refused). `archive(id)` stops new customers using it;
|
|
507
|
+
existing customers keep it.
|
|
508
|
+
- **Assigning existing customers.** `profileTemplateKey` on an individual or
|
|
509
|
+
entity update switches one customer, and the result must carry every
|
|
510
|
+
required field. `customers.assignProfileTemplate({ customerIds,
|
|
511
|
+
profileTemplateKey })` switches up to 500 at once and never refuses on
|
|
512
|
+
required fields: it keeps the values the template can hold, applies
|
|
513
|
+
defaults, and lists each customer's `missingFields`. Such a customer is
|
|
514
|
+
incomplete until a profile edit fills them.
|
|
515
|
+
|
|
516
|
+
```ts
|
|
517
|
+
const { assigned, unchanged, skipped } = await os.customers.assignProfileTemplate({
|
|
518
|
+
customerIds: legacyCustomerIds,
|
|
519
|
+
profileTemplateKey: 'individual-standard',
|
|
520
|
+
});
|
|
521
|
+
const incomplete = assigned.filter((c) => c.missingFields.length > 0);
|
|
522
|
+
```
|
|
523
|
+
- **Creation setup.** `individualCreation` (`INDIVIDUAL` templates) or
|
|
524
|
+
`entityCreation` (`ENTITY` templates) configures the simple Add Individual /
|
|
525
|
+
Add Entity form for the template: on/off and open-by-default, external-ID
|
|
526
|
+
strategy, presets (tags + assignees), and the document or registration
|
|
527
|
+
types offered. The other party type's field is always `null` on responses
|
|
528
|
+
and a 400 on writes. A template created without one copies the org's
|
|
529
|
+
current Simple Customer Creation preference; on `update`, a value fully
|
|
530
|
+
replaces the setup and `null` clears it (the org-wide preference then
|
|
531
|
+
applies). Read shape: `SimpleCustomerCreationPreferences` /
|
|
532
|
+
`SimpleEntityCreationPreferences`; write shape:
|
|
533
|
+
`UpdateSimpleCustomerCreationPreferencesInput` /
|
|
534
|
+
`UpdateSimpleEntityCreationPreferencesInput`.
|
|
535
|
+
|
|
536
|
+
```ts
|
|
537
|
+
await os.config.profileTemplates.update(templateId, {
|
|
538
|
+
individualCreation: {
|
|
539
|
+
enabled: true,
|
|
540
|
+
openByDefault: true,
|
|
541
|
+
externalIdStrategy: 'DOCUMENT',
|
|
542
|
+
documentTypes: [{ label: 'NIC (Sri Lanka)', type: 'NATIONAL_ID', country: 'LKA' }],
|
|
543
|
+
presets: [{ label: 'General Insurance', tags: [{ label: 'General Insurance' }],
|
|
544
|
+
assignedToUserIds: [], assignedToTeamIds: [giTeamId] }],
|
|
545
|
+
allowTags: false,
|
|
546
|
+
allowAssignment: false,
|
|
547
|
+
},
|
|
548
|
+
});
|
|
549
|
+
```
|
|
550
|
+
- Profile fields are **not** readable by rules. Promote a value to a
|
|
551
|
+
`$customer.*` constant if a rule needs it.
|
|
552
|
+
|
|
553
|
+
### Derived inputs
|
|
554
|
+
|
|
555
|
+
A factor can read a value the platform derives itself instead of a profile
|
|
556
|
+
field, by setting `input` instead of `field` (`RiskMatrixDerivedInput`).
|
|
557
|
+
Nobody has to push these as picklist answers, and they re-score on their own
|
|
558
|
+
when the underlying data changes:
|
|
559
|
+
|
|
560
|
+
| `input` | Value | Score it with |
|
|
561
|
+
|---|---|---|
|
|
562
|
+
| `customer.nationality` | ISO-3 codes: an individual's nationalities, an entity's country of incorporation | `levels` per code, or `criteria` against a `$global.*` country list |
|
|
563
|
+
| `customer.productKeys` | Product keys (`syncKey`, else `externalId`) of the customer's accounts that are not closed | `levels` per product key |
|
|
564
|
+
| `customer.expectedMonthlyVolume` | The expected activity profile's monthly volume | `ranges` |
|
|
565
|
+
|
|
566
|
+
When a customer holds several values of a list input, the riskiest scores.
|
|
567
|
+
|
|
568
|
+
```ts
|
|
569
|
+
{ key: 'citizenship', label: 'Citizenship', weight: 0.05, input: 'customer.nationality', whenUnmatched: 'HIGH',
|
|
570
|
+
criteria: [
|
|
571
|
+
{ label: 'FATF grey/black list', level: 'HIGH',
|
|
572
|
+
when: { field: 'customer.nationality', operator: 'IN', value: '$global.fatfGreyBlackCountries' } },
|
|
573
|
+
{ label: 'Any other nationality', level: 'LOW',
|
|
574
|
+
when: { field: 'customer.nationality', operator: 'IS_NOT_EMPTY', value: '' } },
|
|
575
|
+
] }
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
Updating the `$global.fatfGreyBlackCountries` constant re-scores everyone in
|
|
579
|
+
the next daily sweep. Changes to the expected activity profile re-score at once;
|
|
580
|
+
accounts opened or closed are picked up by the daily sweep.
|
|
581
|
+
|
|
582
|
+
### Dry runs: preview and impact
|
|
583
|
+
|
|
584
|
+
Score an unsaved matrix without saving it. The draft is validated exactly as a
|
|
585
|
+
save would be (400 lists every problem), and both calls compare it with the
|
|
586
|
+
template's saved matrix evaluated on the same inputs.
|
|
587
|
+
|
|
588
|
+
```ts
|
|
589
|
+
// One customer, draft next to the saved matrix
|
|
590
|
+
const preview = await os.config.profileTemplates.previewRiskMatrix({
|
|
591
|
+
customerId, fieldConfig: draft.fieldConfig, riskMatrix: draft.riskMatrix,
|
|
592
|
+
});
|
|
593
|
+
preview.published?.finalRating; // 'HIGH'
|
|
594
|
+
preview.draft.finalRating; // 'MEDIUM'
|
|
595
|
+
|
|
596
|
+
// Every live customer on the template
|
|
597
|
+
const impact = await os.config.profileTemplates.riskMatrixImpact(templateId, {
|
|
598
|
+
fieldConfig: draft.fieldConfig, riskMatrix: draft.riskMatrix,
|
|
599
|
+
});
|
|
600
|
+
impact.changed; // customers whose rating would move
|
|
601
|
+
impact.movements; // [{ from: 'LOW', to: 'MEDIUM', count: 312 }, ...]
|
|
602
|
+
impact.customers; // who moves, rises before falls, biggest change first (at most 100)
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
Customers with an officer override are counted in `overriddenChanged`; their
|
|
606
|
+
rating would not move. Up to 5,000 customers are scored per call (`truncated`
|
|
607
|
+
says when there were more).
|
|
608
|
+
|
|
609
|
+
### KYC output mappings
|
|
610
|
+
|
|
611
|
+
A template's KYC binding can write a verified session's answers back to the
|
|
612
|
+
customer. Each mapping reads one session value and writes it to a profile field
|
|
613
|
+
(`fields.<key>`) or a customer-level constant (`$customer.<name>`). Constants
|
|
614
|
+
are the way to make a KYC answer readable by rules; profile fields never are.
|
|
615
|
+
|
|
616
|
+
```ts
|
|
617
|
+
await os.config.profileTemplates.update(templateId, {
|
|
618
|
+
kycWorkflows: [{
|
|
619
|
+
purpose: 'ONBOARDING',
|
|
620
|
+
providerConfigId,
|
|
621
|
+
workflowId: 'wf_onboarding',
|
|
622
|
+
mappings: [
|
|
623
|
+
{ source: 'form.occupation', target: 'fields.occupation',
|
|
624
|
+
valueMap: [{ from: 'Software engineer', to: 'IT / Software Professional' }] },
|
|
625
|
+
{ source: 'session.verified', target: 'fields.onboardingChannel',
|
|
626
|
+
valueMap: [{ from: 'true', to: 'Non Face to Face Clients' }], mode: 'OVERWRITE' },
|
|
627
|
+
{ source: 'person.nationality', target: '$customer.kycNationalities' },
|
|
628
|
+
],
|
|
629
|
+
}],
|
|
630
|
+
});
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
- Mappings run once, when a session of that workflow becomes verified (the
|
|
634
|
+
provider approved it, or an officer approved it in the OS).
|
|
635
|
+
- `FILL` (default) writes only an empty target; `OVERWRITE` replaces it.
|
|
636
|
+
- A value the target cannot hold (e.g. an answer that is not one of the field's
|
|
637
|
+
options) is skipped; the rest still apply.
|
|
638
|
+
- The save validates sources, targets, options and constants, and lists every
|
|
639
|
+
problem in one 400.
|
|
640
|
+
- Constant writes carry the session id as their reason in the constant history.
|
|
641
|
+
Any change re-scores the customer.
|
|
642
|
+
|
|
643
|
+
### Periodic reviews
|
|
644
|
+
|
|
645
|
+
Every matrix run keeps the customer's `nextReviewAt` (on individual and
|
|
646
|
+
entity responses) in line with the review cycle of the rating in force
|
|
647
|
+
(`outcomes[].reviewEveryMonths`). The clock starts at the first rating. A rise
|
|
648
|
+
to a rating with a shorter cycle brings the review forward; a fall leaves it
|
|
649
|
+
where it is. When the date arrives, the daily sweep opens a Task labelled
|
|
650
|
+
`periodic-review`, assigned to the customer's assignees, due on the review
|
|
651
|
+
date and linked to the customer. Closing that Task (done or cancelled)
|
|
652
|
+
restarts the clock from that day.
|
|
653
|
+
|
|
654
|
+
### Override disagreement
|
|
655
|
+
|
|
656
|
+
While an officer override is in force, the matrix keeps scoring. When its
|
|
657
|
+
rating moves away from the override, the customer's assignees get a
|
|
658
|
+
notification ("The risk matrix disagrees with an officer-set rating"). It is
|
|
659
|
+
sent once per change, not on every run.
|
|
660
|
+
|
|
661
|
+
Spec: [`docs/specs/profile-templates/README.md`](../../docs/specs/profile-templates/README.md).
|
|
662
|
+
|
|
376
663
|
## Tenant sync — config-as-code (BREAKING in v0.7.0)
|
|
377
664
|
|
|
378
665
|
`os.config.sync` drives declarative tenant config (products, constants, event
|
|
379
|
-
definitions, webhooks, rules, currencies, and tenant behaviour settings). You build a `SyncConfig` and the
|
|
666
|
+
definitions, webhooks, rules, currencies, profile templates, and tenant behaviour settings). You build a `SyncConfig` and the
|
|
380
667
|
**server** validates, diffs, and applies it — clients hold no diff/apply logic.
|
|
381
668
|
This backs the `dossiers-sync` CLI.
|
|
382
669
|
|
|
@@ -439,12 +726,14 @@ const { tokens } = await os.config.tagCatalogue.palette(); // valid `color` valu
|
|
|
439
726
|
> accepted. Product DTOs no longer carry `metadata` — use `otherProperties`
|
|
440
727
|
> instead.
|
|
441
728
|
|
|
442
|
-
Accounts, products, and event ingestion accept the platform's Typed Fields
|
|
729
|
+
Accounts, products, customers, and event ingestion accept the platform's Typed Fields
|
|
443
730
|
primitive — a typed key/value bag (`fields`) plus an untyped escape hatch
|
|
444
731
|
(`otherProperties`). Collection fields are explicit: `LIST` is open-ended,
|
|
445
732
|
while `SINGLE_SELECT` and `MULTI_SELECT` are constrained by the declared
|
|
446
|
-
`options` on the parent schema (`Product.accountFieldConfig
|
|
447
|
-
`EventDefinition.properties`). Legacy `SET`
|
|
733
|
+
`options` on the parent schema (`Product.accountFieldConfig`,
|
|
734
|
+
`ProfileTemplate.fieldConfig` or `EventDefinition.properties`). Legacy `SET`
|
|
735
|
+
remains supported but is deprecated. Customer `fields` are always Tier A — see
|
|
736
|
+
[Profile templates & risk matrix](#profile-templates--risk-matrix).
|
|
448
737
|
|
|
449
738
|
```ts
|
|
450
739
|
import type { TypedFieldValueInput } from '@dossier-wiki/os-sdk';
|
|
@@ -536,7 +825,9 @@ The application uses the same disposition cards, reason field, and optional cata
|
|
|
536
825
|
|
|
537
826
|
New workflow saves require one shared **Resolved** stage. Set its `next` to a router or an automatic `action` node for follow-ups. Routers can combine `resolution.outcome` with alert, customer, screening or rule fields; after resolution, `$end` means no further action. Action nodes support `CREATE_TASK` (linked to the alert) and `NOTIFY` (in-app notification), with current alert owners or specific members/teams as recipients. Follow-ups run atomically with closure using the saved verdict and leave the alert resolved. Reopen remains a separate review action. Reopening and resolving again can run follow-ups again; retrying the same transition cannot duplicate them. Existing alerts retain their pinned definition.
|
|
538
827
|
|
|
539
|
-
Each organization has one workflow per alert type: `SCREENING` and `
|
|
828
|
+
Each organization has one workflow per alert type: `SCREENING`, `RULE_TRIGGER` and `CUSTOM`. Initial assignment and later handovers are stage settings; routers provide ordered conditions and a fallback. Router branches may have an optional `name`, and the router may have a `fallbackName` (nonblank, up to 200 characters). These names appear in the workflow graph and routing history; they do not change branch IDs, order, or destinations. Omitting them preserves the numbered condition and fallback labels.
|
|
829
|
+
|
|
830
|
+
Routers can also read `dependencies.openCount`, the number of **blocking alerts** that are still open (see [Blocking alerts](#blocking-alerts)); `0` means the alert is free to be resolved. Conditions (`WorkflowConditions`) are a tree. `conditions` holds leaf conditions and nested groups, each group with its own `logic` of `AND` or `OR`, so "high risk and (ID matched or DOB matched)" is `{ logic: 'AND', conditions: [leaf, { logic: 'OR', conditions: [leaf, leaf] }] }`. Groups nest at most three levels below the root and hold at most 30 leaves in total; a nested group must contain at least one entry. Flat lists saved by older clients remain valid. Screening workflows can also route on `screening.maxNameMatchPercent`, the strongest name match (0–100, whole number) across the alert's matches that are not marked false positive; it is unavailable (`null`) when no match carries a name score. Read the current revision before saving:
|
|
540
831
|
|
|
541
832
|
```ts
|
|
542
833
|
const workflow = await client.config.alertWorkflows.get('SCREENING');
|
|
@@ -553,6 +844,8 @@ Requires `config:read` / `config:write`. Stale saves return HTTP 409. Existing a
|
|
|
553
844
|
|
|
554
845
|
Set `definition.restrictEditingToOwners` to `true` to make alerts read-only for everyone except currently assigned people and members of active assigned teams, including administrators. This covers assignments, match decisions, alert comments and attachments, task links, and every stage transition (including return and reopen), overriding the progression-only policy. Omitting it or setting it to `false` preserves existing editing permissions. The policy is pinned when the alert is created; publishing does not change older alerts or Simple alerts. Linked tasks retain their own editing permissions. Organization admins and owners can explicitly add themselves through the application’s **Assign to me** action (`takeOverAlert` in GraphQL). This administrative recovery preserves all other assignees, status and pinned workflow settings, and is recorded in activity history; it also supports resolved alerts so an administrator can assume ownership before reopening.
|
|
555
846
|
|
|
847
|
+
Set `definition.restrictReportsToClosedAlerts` to `true` to allow PDF reports only once an alert is closed. While such an alert is open, `GET /v1/reports/alerts/{id}/pdf` returns HTTP 403, and `GET /v1/reports/customers/{id}/pdf` returns HTTP 403 while the customer has any open alert under this policy. GraphQL exposes the reason as `Alert.reviewWorkflow.reportRestriction` and `Customer.reportExportRestriction` (both null when a report can be generated). Like the other policies it is pinned at alert creation; Simple alerts are never restricted.
|
|
848
|
+
|
|
556
849
|
Set `definition.restrictProgressionToOwners` to `true` to limit forward progression (including resolution) to currently assigned people and members of active assigned teams. Administrators must also be assigned. Set it to `false` to allow other organization members with `alert:update` permission to progress alerts. Stage requirements still apply. Return and reopen permissions are unchanged. Omitting the field preserves the original current-reviewers-or-administrators behavior for older API clients and stored definitions. The builder defaults the policy to enabled when editing; publishing affects new alerts only.
|
|
557
850
|
|
|
558
851
|
For Screening workflows, `definition.matchAssessment` sets shared `{ note, attachment }` booleans for `CONFIRMED`, `INCONCLUSIVE`, and `FALSE_POSITIVE`. These apply to every Initial Review stage and to Secondary Review when `definition.allowSecondaryReviewMatchOverrides` is `true`; set each stage's legacy `screening` field to `null`. Enabling overrides requires a shared `matchAssessment` policy. Initial Review reviewers can replace individual match verdicts directly without resetting to pending. Authorized secondary reviewers can then also replace a match verdict directly, with the required evidence and an audit record of the previous verdict. Existing reviewer and editing permissions still apply; resolved alerts remain read-only for match decisions. Overrides default to off, and non-screening workflows must omit both settings. Older definitions that omit `matchAssessment` retain their stage-specific requirements. The builder's General policy tab consolidates older stage requirements into a shared draft, preserving the strictest requirement where they differ. Publishing never updates existing alerts.
|
|
@@ -566,3 +859,32 @@ Attach screening matches explicitly with `links: { matchIds: ["<match UUID>"] }`
|
|
|
566
859
|
Use `client.config.alertWorkflows.getExperience()` to read the tenant's choice and revision. `saveExperience({ experience: 'SIMPLE' | 'DEFINED', expectedRevision })` changes only newly created alerts, across both supported alert types. Simple uses direct assignment and Open/Closed resolution; Defined uses the published workflow for the alert type. Existing alerts retain their process and policy version. Saved definitions are retained in Simple mode, and publishing a definition does not enable Defined mode. The initial default is Simple. Select Defined explicitly to enable the published workflow for new alerts. Existing saved choices are preserved when upgrading.
|
|
567
860
|
|
|
568
861
|
Published workflow history is available through `config.alertWorkflows.versions(alertType)` (newest first, with publisher and date). `config.alertWorkflows.restore(alertType, { version, expectedRevision })` copies a version into the draft without changing the published workflow or existing alerts. Review the returned draft, then call `save` with `publish: true` and its updated revision to publish it as a new version. Old versions with no recoverable definition return `definition: null` and cannot be restored. Both operations are organization-scoped and require `config:read` / `config:write` respectively.
|
|
862
|
+
|
|
863
|
+
### Blocking alerts
|
|
864
|
+
|
|
865
|
+
An alert can be made to **wait on** another alert. The alert that must close first is the **blocking alert**; the one that waits is the **blocked alert**. The rule is small: **an alert with an open blocking alert cannot be resolved or closed.** Everything else — assignment, comments, attachments, notes, match decisions, stage progression short of resolution — stays allowed. Closing or resolving a blocked alert returns HTTP 400 naming how many blockers are still open. Once the last blocker closes, the blocked alert's owners are notified and it can be resolved.
|
|
866
|
+
|
|
867
|
+
Links are directed and acyclic: an alert cannot block itself, and a link that would form a loop (the blocking alert already waits, directly or through other alerts, on the blocked one) is rejected with HTTP 400. Links are kept after either alert closes, so the history of why an alert waited stays readable; only *open* blockers block. Links come from three places: entity screening (an entity's screening alert is blocked by each of its stakeholders' screening alerts), analysts in the app, and the API below.
|
|
868
|
+
|
|
869
|
+
```ts
|
|
870
|
+
// Make alert A wait on alert B. Idempotent — re-adding returns the existing link.
|
|
871
|
+
const link = await client.alerts.dependencies.add(alertA, { blockedByAlertId: alertB });
|
|
872
|
+
// { id, alertId: alertA, blockedByAlertId: alertB, createdBy: { userId, name }, createdAt }
|
|
873
|
+
|
|
874
|
+
// Both sides of A's links, oldest first. Compare each linked alert's status to see which still block.
|
|
875
|
+
const { blockedBy, blocks } = await client.alerts.dependencies.list(alertA);
|
|
876
|
+
|
|
877
|
+
// Remove the link. Removing a link that does not exist changes nothing (204 either way).
|
|
878
|
+
await client.alerts.dependencies.remove(alertA, alertB);
|
|
879
|
+
|
|
880
|
+
// `isBlocked` is on the flat alert view.
|
|
881
|
+
const alert = await client.alerts.get(alertA); // alert.isBlocked === true while B is open
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
REST: `GET /v1/alerts/{id}/dependencies` → `{ blockedBy: AlertDependency[], blocks: AlertDependency[] }`; `POST /v1/alerts/{id}/dependencies` with `{ blockedByAlertId }` → the link (201); `DELETE /v1/alerts/{id}/dependencies/{blockedByAlertId}` → 204. Reads need `alerts:read`, writes `alerts:write`. A session user is held to the blocked alert's owner-only editing policy (`restrictEditingToOwners`); an API key acts as the key and the link's `createdBy` names the key.
|
|
885
|
+
|
|
886
|
+
GraphQL exposes the same links on every alert type as `blockedBy: [AlertDependency!]!`, `blocks: [AlertDependency!]!` and `isBlocked: Boolean!`, where `AlertDependency { id, alertId, blockedByAlertId, createdBy, createdAt, blockedAlert, blockingAlert }` resolves both alerts in full. The mutations are `addAlertDependency(alertId, blockedByAlertId)` and `removeAlertDependency(alertId, blockedByAlertId)`, both returning the blocked alert and requiring `alert:update`. The alert list filter accepts `blocked: Boolean` to show only alerts that are (or are not) currently blocked.
|
|
887
|
+
|
|
888
|
+
Workflows see the count as the `dependencies.openCount` field. Screening's automatic close (every match eliminated by the provider) skips a blocked alert; its owners resolve it once the blocking alerts have closed.
|
|
889
|
+
|
|
890
|
+
**Stakeholder screening.** When an entity is screened, every stakeholder (director, UBO, shareholder) is rescreened with it, except stakeholders screened within the last `stakeholderRescreenSkipHours` — a screening setting on `ScreeningSettings` (GraphQL `screeningSettings` / `updateScreeningSettings`), default `24`, `0` = always rescreen, maximum 90 days (`2160`). The entity then holds one ordinary Screening alert — the one its own matches raised, or one with zero matches of its own when the entity was clean — blocked by each stakeholder's open screening alert, with its risk level held at the highest risk among its open blockers. A clean entity's alert stays open until a person resolves it once the last stakeholder alert has closed; its owners are notified at that point.
|
|
@@ -9,10 +9,17 @@ export interface WorkflowCondition {
|
|
|
9
9
|
valueField?: string;
|
|
10
10
|
whenMissing?: string;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* A condition group. `conditions` holds leaf conditions and nested groups, so
|
|
14
|
+
* "A and (B or C)" is `{ logic: "AND", conditions: [A, { logic: "OR", conditions: [B, C] }] }`.
|
|
15
|
+
* Groups nest at most three levels below the root and hold at most 30 leaves in
|
|
16
|
+
* total. A nested group needs at least one entry; an empty root matches every alert.
|
|
17
|
+
*/
|
|
12
18
|
export interface WorkflowConditions {
|
|
13
19
|
logic: "AND" | "OR";
|
|
14
|
-
conditions:
|
|
20
|
+
conditions: WorkflowConditionNode[];
|
|
15
21
|
}
|
|
22
|
+
export type WorkflowConditionNode = WorkflowCondition | WorkflowConditions;
|
|
16
23
|
export interface WorkflowAssignment {
|
|
17
24
|
mode: "KEEP" | "HANDOVER" | "ADD";
|
|
18
25
|
source: "SPECIFIC" | "CUSTOMER_MEMBERS" | "CUSTOMER_TEAMS" | "CUSTOMER_OWNERS" | "ORIGINAL";
|
|
@@ -37,6 +44,11 @@ export interface WorkflowStage {
|
|
|
37
44
|
requiredReviews: ("CUSTOMER_RISK" | "COUNTERPARTY_RISK")[];
|
|
38
45
|
/** Legacy stage policy. New definitions use matchAssessment and set this to null. */
|
|
39
46
|
screening: MatchAssessmentPolicy | null;
|
|
47
|
+
/** What happens to assignees when an alert is returned to this stage.
|
|
48
|
+
* RULE (omission): run this stage's assignment rule. RESTORE: reassign to whoever held the
|
|
49
|
+
* alert when it last left this stage. ASK: the returning reviewer chooses, defaulting to restore.
|
|
50
|
+
* Never set on a Resolved stage: nothing is returned to it. */
|
|
51
|
+
returnAssignment?: "RULE" | "RESTORE" | "ASK";
|
|
40
52
|
}
|
|
41
53
|
export interface WorkflowRouter {
|
|
42
54
|
id: string;
|
|
@@ -80,6 +92,10 @@ export interface WorkflowDefinition {
|
|
|
80
92
|
matchAssessment?: MatchAssessmentPolicy;
|
|
81
93
|
/** Screening only. Allow reviewers to override match verdicts in Secondary Review using matchAssessment requirements. Omission/false keeps decisions in Initial Review. */
|
|
82
94
|
allowSecondaryReviewMatchOverrides?: boolean;
|
|
95
|
+
/** PDF reports of an alert can be generated only once it is closed, and a customer profile
|
|
96
|
+
* cannot be exported while the customer has an open alert under this policy. Pinned at alert
|
|
97
|
+
* creation like the other policies. Omission/false allows reports at any time. */
|
|
98
|
+
restrictReportsToClosedAlerts?: boolean;
|
|
83
99
|
entry: string;
|
|
84
100
|
/** Exactly one Resolved stage. Its optional next step runs automatically after closure.
|
|
85
101
|
* Routers and actions after resolution may use "$end" to finish without another action. */
|
|
@@ -111,5 +127,12 @@ export interface WorkflowExecution {
|
|
|
111
127
|
enteredAt: string;
|
|
112
128
|
initialUserIds: string[];
|
|
113
129
|
initialTeamIds: string[];
|
|
130
|
+
/** Who held the alert when it last left each stage, by node id. Written on every
|
|
131
|
+
* transition out of a stage; absent on alerts enrolled before it existed. */
|
|
132
|
+
stageAssignees?: Record<string, {
|
|
133
|
+
userIds: string[];
|
|
134
|
+
teamIds: string[];
|
|
135
|
+
leftAt: string;
|
|
136
|
+
}>;
|
|
114
137
|
}
|
|
115
138
|
//# sourceMappingURL=alert-workflow-types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"alert-workflow-types.d.ts","sourceRoot":"","sources":["../src/alert-workflow-types.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG,cAAc,GAAG,QAAQ,CAAC;AACxE,MAAM,MAAM,kBAAkB,GAC1B,gBAAgB,GAChB,kBAAkB,GAClB,UAAU,CAAC;AACf,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AACD,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,UAAU,EAAE,
|
|
1
|
+
{"version":3,"file":"alert-workflow-types.d.ts","sourceRoot":"","sources":["../src/alert-workflow-types.ts"],"names":[],"mappings":"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"}
|