import { Callout } from "zudoku/ui/Callout";

# Sanction Screening Source

365 business Sanction Screen screens customers, vendors, contacts, employees, bank accounts, addresses and sales and purchase documents. If you need an entity it does not cover - a table of your own, or a standard table nobody has connected yet - you add it as a **sanction screening source**.

A source consists of two things: a value in the `bdev.Sanction Screening Source` enum, and a codeunit implementing `bdev.ISanctionScreening` that the enum points at. Everything else - the log, the audit report, the gap analysis, the retention policy - follows from that.

<Callout type="info" title="Version">
Adding a source needs 365 business Sanction Screen **18.3** or newer. The screening methods an implementation calls became public with that version.
</Callout>

The examples below connect a table `Broker` with the id `50100`.

## Step 1: extend the enum

```pascal
enumextension 50100 "My Sanction Screening Source" extends "bdev.Sanction Screening Source"
{
    value(50100; Broker)
    {
        Caption = 'Broker';
        Implementation = "bdev.ISanctionScreening" = "My Sanction Screen - Broker";
    }
}
```

<Callout type="caution" title="The enum value must be the table id">
The product uses the enum value as a table id - the gap analysis of the audit report opens the source table with it, and so does the screening log when it resolves a record. `Customer` is `18`, `Vendor` is `23`. Give your value the id of your source table, or those features will fail on your entity.
</Callout>

## Step 2: implement the interface

The interface has eight methods. Most of them are answered by forwarding to `bdev.Sanction Screen Integ.`; only building the screening model and walking your table for the batch are yours to write.

```pascal
codeunit 50101 "My Sanction Screen - Broker" implements "bdev.ISanctionScreening"
{
    Access = Public;

    var
        SanctionScreening: Codeunit "bdev.Sanction Screen Integ.";

    procedure MatchBatch(interval: Enum "bdev.OS Screening Interval"; excludeEntitiesWithoutTransactions: Boolean)
    var
        broker: Record Broker;
    begin
        broker.Reset();
        if (not broker.FindSet(false)) then
            exit;

        repeat
            if (NeedsScreening(broker, interval)) then
                Match(broker);
        until broker.Next() = 0;
    end;

    procedure Match(recordVariant: Variant): Boolean
    var
        broker: Record Broker;
        typeHelper: Codeunit "Data Type Management";
        recRef: RecordRef;
        recordMustBeBrokerErr: Label 'The record to screen must be a Broker record.';
    begin
        typeHelper.GetRecordRef(recordVariant, recRef);
        if (recRef.Number() <> Database::Broker) then
            Error(recordMustBeBrokerErr);

        recRef.SetTable(broker);
        exit(SanctionScreening.Match(ToScreeningModel(broker)));
    end;

    procedure GetRecordID(systemId: Guid): Text
    var
        recRef: RecordRef;
    begin
        recRef.Open(Database::Broker);
        recRef.GetBySystemId(systemId);
        exit(Format(recRef.RecordId));
    end;

    procedure GetLastScreeningResult(systemId: Guid): Record "bdev.OS Screening Result"
    var
        broker: Record Broker;
    begin
        if (IsNullGuid(systemId)) then
            exit;

        broker.GetBySystemId(systemId);
        exit(SanctionScreening.GetLastScreeningResult(
            Enum::"bdev.Sanction Screening Source"::Broker, systemId, ToScreeningModel(broker).Join()));
    end;

    procedure GetLastDateOfScreening(systemId: Guid): Date
    begin
        exit(SanctionScreening.GetLastDateOfScreening(Enum::"bdev.Sanction Screening Source"::Broker, systemId));
    end;

    procedure GetMatches(var sanctionEntry: Record "bdev.OS Match Entry"): Boolean
    begin
        exit(SanctionScreening.GetMatches(sanctionEntry));
    end;

    procedure ShowMatches()
    var
        sanctionEntry: Record "bdev.OS Match Entry";
    begin
        SanctionScreening.GetMatches(sanctionEntry);
        Page.Run(Page::"bdev.OS Sanction Entries", sanctionEntry);
    end;

    procedure GetNoOfMatches(): Integer
    begin
        exit(SanctionScreening.GetNoOfMatches());
    end;
}
```

### Building the screening model

This is the part that decides what is actually checked - and, because the screened data is recorded as evidence, what an audit will later see as "checked data".

```pascal
    local procedure ToScreeningModel(broker: Record Broker) screeningRecord: Record "bdev.Sanction Screening Model" temporary
    begin
        screeningRecord."Entity Type" := Enum::"bdev.OS Entity Type"::Organization;
        screeningRecord.Validate(Name, broker.Name);
        screeningRecord.Validate(Address, broker.Address);
        screeningRecord.Validate("Post Code", broker."Post Code");
        screeningRecord.Validate(City, broker.City);
        screeningRecord.Validate("Country ISO Code",
            SanctionScreening.GetISOCodeFromCountryRegionCode(broker."Country/Region Code"));

        screeningRecord."Source Type" := Enum::"bdev.Sanction Screening Source"::Broker;
        screeningRecord."Source System ID" := broker.SystemId;
    end;
```

| Field | Type | Notes |
| --- | --- | --- |
| `Entity Type` | Enum "bdev.OS Entity Type" | `Organization`, `Person` or `LegalEntity`. Determines how the data sources interpret the name. |
| `Name` | Text[1024] | Required. Nothing is screened without it. |
| `Address`, `Post Code`, `City`, `County` | | The address as it stands on the record. |
| `Country ISO Code` | Code[2] | Two letter code. Use `GetISOCodeFromCountryRegionCode` to convert a Business Central country code. |
| `Birth Date`, `Birth Country ISO Code` | | For a person. Sharpens the result considerably. |
| `Registration No.`, `VAT Registration No.` | | For a company. Sharpens the result considerably. |
| `Source Type`, `Source System ID` | | Required. They tie the screening to your record. |

<Callout type="tip" title="Use Validate">
`Validate` strips characters the data sources cannot handle and normalises the value. Assigning the fields directly skips that.
</Callout>

### Deciding when a record is due

`MatchBatch` receives the interval that is configured for your source type. Whether a single record is due follows from its last screening:

```pascal
    internal procedure NeedsScreening(broker: Record Broker; interval: Enum "bdev.OS Screening Interval"): Boolean
    var
        lastScreeningDate: Date;
    begin
        lastScreeningDate := DT2Date(GetLastScreeningResult(broker.SystemId)."Last Updated");
        if (lastScreeningDate = 0D) then
            exit(true);

        exit(SanctionScreening.GetNextScreeningDate(lastScreeningDate, interval) <= Today());
    end;
```

## Step 3: put the screening on the card

Users reach the screening from the card of the record. The pattern is the same for every entity: the indicator factbox, an action to screen, and an action to open the log.

```pascal
pageextension 50102 "My Broker Card" extends "Broker Card"
{
    layout
    {
        addfirst(factboxes)
        {
            part("bdev.OS Screening Indicator"; "bdev.OS Screening Indicator")
            {
                ApplicationArea = All;
                Caption = 'Sanction Screening';
                SubPageLink = "Source System ID" = field(SystemId),
                              "Source Type" = const(Broker);
            }
        }
    }
    actions
    {
        addlast(processing)
        {
            action(SanctionScreening)
            {
                ApplicationArea = All;
                Caption = 'Sanction Screening';
                ToolTip = 'Performs a sanction screening for the selected broker.';
                Ellipsis = true;
                Image = AddWatch;

                trigger OnAction()
                var
                    sourceType: Enum "bdev.Sanction Screening Source";
                    screening: Interface "bdev.ISanctionScreening";
                    noMatchesMsg: Label 'Sanctions check completed successfully.\No potential matches have been found.';
                    matchesQst: Label 'Sanction screening found %1 possible matches.\Do you want to show the Sanction Entries?', Comment = '%1 = No. of Matches';
                begin
                    sourceType := sourceType::Broker;
                    screening := sourceType;
                    if (not screening.Match(Rec)) then begin
                        Message(noMatchesMsg);
                        exit;
                    end;

                    if (Confirm(matchesQst, false, screening.GetNoOfMatches())) then
                        screening.ShowMatches();
                end;
            }
        }
    }
}
```

Note how the action never names your implementation codeunit: it takes the interface from the enum value. Any code that knows the source type can screen your entity this way, including code in other extensions.

## Step 4: run it automatically

This is the one place where your source is not treated like a built-in one. The job queue entry the product ships runs a report with a fixed data item per built-in entity; it does not walk the enum, so it will not screen your table.

Add a processing-only report of your own and schedule it:

```pascal
report 50103 "My Broker Screening"
{
    ProcessingOnly = true;
    UseRequestPage = false;

    trigger OnPostReport()
    var
        sanctionScreening: Codeunit "bdev.Sanction Screen Integ.";
        sourceType: Enum "bdev.Sanction Screening Source";
        screening: Interface "bdev.ISanctionScreening";
    begin
        sourceType := sourceType::Broker;
        screening := sourceType;
        screening.MatchBatch(sanctionScreening.GetScreeningInterval(sourceType), false);
    end;
}
```

`GetScreeningInterval` returns the interval an administrator configured for your source type on the **Sanction Screen Setup**, so your entity is set up in the same place as every other one.

<Callout type="tip" title="This example is compiled">
The code on this page is built against the app as part of its own development, so it does compile. Only the `Broker` table and its card are stand-ins for whatever you are connecting.
</Callout>

## What you get without doing anything

Because the product iterates over the enum rather than over a fixed list, your entity is covered by the parts that matter for an audit:

| | |
| --- | --- |
| **Screening log** | Every screening of your entity is logged with the full screening context: threshold, algorithm, the data sources it ran against, and how current those were. |
| **Audit report** | Your screenings appear under the screenings performed, your matches under the matches and decisions. |
| **Gap analysis** | The audit report opens your source table and reports records that were never screened or whose interval has elapsed. This is why the enum value has to be the table id. |
| **Retention policy** | The log entries of your entity are governed by the same retention policy as all others. |
| **Events** | `OnBeforeMatchRecord` and `OnAfterInitializeModelFromSource` fire for your entity as well, so other extensions can enrich or skip your screenings. |

<Callout type="caution" title="The gap analysis has no filters for your table">
For customers, vendors and bank accounts the gap analysis leaves out blocked records, because the product does not screen those either. It cannot know the equivalent rule for your table, so every record of it is expected to be screened. Keep that in mind if your table holds records that are never meant to be screened.
</Callout>

## See also

- [365 business Sanction Screen - Overview](readme.mdx)
- [Extensibility Events](extensibility-events.mdx)
- [Documentation - Audit Report](../../en-us/365-business-sanction-screen/audit-report.mdx)
