Security change on December 1, 2026: TLS 1.2 will become the minimum standard for 365 business Print Agent. Details and system requirements
365 business development
365 business Sanction Screen

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.

Version

Adding a source needs 365 business Sanction Screen 18.3 or newer. The screening methods an implementation calls became public with that version.

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

Step 1: extend the enum

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

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.

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.

Code
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".

Code
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;
FieldTypeNotes
Entity TypeEnum "bdev.OS Entity Type"Organization, Person or LegalEntity. Determines how the data sources interpret the name.
NameText[1024]Required. Nothing is screened without it.
Address, Post Code, City, CountyThe address as it stands on the record.
Country ISO CodeCode[2]Two letter code. Use GetISOCodeFromCountryRegionCode to convert a Business Central country code.
Birth Date, Birth Country ISO CodeFor a person. Sharpens the result considerably.
Registration No., VAT Registration No.For a company. Sharpens the result considerably.
Source Type, Source System IDRequired. They tie the screening to your record.

Use Validate

Validate strips characters the data sources cannot handle and normalises the value. Assigning the fields directly skips that.

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:

Code
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.

Code
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:

Code
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.

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.

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 logEvery 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 reportYour screenings appear under the screenings performed, your matches under the matches and decisions.
Gap analysisThe 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 policyThe log entries of your entity are governed by the same retention policy as all others.
EventsOnBeforeMatchRecord and OnAfterInitializeModelFromSource fire for your entity as well, so other extensions can enrich or skip your screenings.

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.

See also

Last modified on