Codeunit 1711 Positive Pay Export Mgt
- App
- Base Application
- Namespace
- Microsoft.Bank.PositivePay
- Versions
- 17-28
Versions171819202122232425262728
Source29
Source in 29
src/Layers/W1/BaseApp/Bank/PositivePay/PositivePayExportMgt.Codeunit.al423 lines, Copyright (c) Microsoft Corporation. MIT
// ------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
// ------------------------------------------------------------------------------------------------
namespace Microsoft.Bank.PositivePay;
using System.IO;
using System.Text;
/// <summary>
/// Manages the export of positive pay data to external file formats, handling data exchange processes for positive pay checks.
/// This codeunit orchestrates the complete export workflow including file generation, data mapping, and validation.
/// </summary>
/// <remarks>
/// The Positive Pay Export Management codeunit is responsible for converting positive pay entries into various export formats
/// required by different banks. It handles the data exchange framework operations including field mapping, data transformation,
/// and file formatting. The codeunit supports fixed-width and delimited file formats and ensures data integrity through
/// validation processes. It integrates with the Data Exchange framework to provide flexible export capabilities.
/// </remarks>
codeunit 1711 "Positive Pay Export Mgt"
{
Permissions = TableData "Data Exch." = rimd,
TableData "Data Exch. Field" = rimd,
TableData "Positive Pay Header" = rimd,
TableData "Positive Pay Footer" = rimd;
trigger OnRun()
begin
end;
var
LineType: Option Detail,Header,Footer;
FormatNotDefinedErr: Label 'You must choose a valid export format for the bank account. Format %1 is not correctly defined.', Comment = '%1 = Data Exch. Def. Code';
DataExchLineDefNotFoundErr: Label 'The %1 export format does not support the Payment Method Code %2.', Comment = '%1=Data Exch. Def. Name;%2=Data Exch. Line Def. Code';
IncorrectLengthOfValuesErr: Label 'The payment that you are trying to export is different from the specified %1, %2.\\The value in the %3 field does not have the length that is required by the export format. \Expected: %4 \Actual: %5 \Field Value: %6.', Comment = '%1=Data Exch.Def Type;%2=Data Exch. Def Code;%3=Field;%4=Expected length;%5=Actual length;%6=Actual Value';
/// <summary>
/// Exports data exchange records to a flat file format with proper formatting and line structure.
/// </summary>
/// <param name="DataExchNo">The data exchange entry number to export.</param>
/// <param name="Filename">The target file path for the export.</param>
/// <param name="LineFileType">The type of line being exported (Detail, Header, or Footer).</param>
/// <param name="HeaderCount">The number of header records processed.</param>
/// <remarks>
/// This procedure handles the physical file creation and writing process for positive pay exports.
/// It manages file appending for multiple line types and ensures proper line termination with CRLF.
/// The procedure integrates with the Export Generic Fixed Width XMLport for data formatting.
/// </remarks>
[Scope('OnPrem')]
procedure ExportDataExchToFlatFile(DataExchNo: Integer; Filename: Text; LineFileType: Integer; HeaderCount: Integer)
var
DataExchField: Record "Data Exch. Field";
DataExch: Record "Data Exch.";
ExportGenericFixedWidth: XMLport "Export Generic Fixed Width";
ExportFile: File;
OutStream: OutStream;
InStream: InStream;
CRLF: Text;
begin
DataExchField.SetRange("Data Exch. No.", DataExchNo);
if DataExchField.Count > 0 then begin
ExportFile.WriteMode := true;
ExportFile.TextMode := true;
if Exists(Filename) and ((LineFileType <> LineType::Header) or ((LineFileType = LineType::Header) and (HeaderCount > 1))) then
ExportFile.Open(Filename)
else
ExportFile.Create(Filename);
DataExch.Get(DataExchNo);
// Copy current file contents to Record
ExportFile.CreateInStream(InStream);
DataExch."File Content".CreateOutStream(OutStream);
CopyStream(OutStream, InStream);
ExportFile.Close();
// Copy current Record to newly-instantiated file
ExportFile.Create(Filename);
DataExch."File Content".CreateInStream(InStream);
ExportFile.CreateOutStream(OutStream);
CopyStream(OutStream, InStream);
if (ExportFile.Len > 0) and
((LineFileType <> LineType::Header) or ((LineFileType = LineType::Header) and (HeaderCount > 1)))
then begin
// Only the first line needs to not write a CRLF.
CRLF[1] := 13;
CRLF[2] := 10;
OutStream.Write(CRLF[1]);
OutStream.Write(CRLF[2]);
end;
if LineFileType = LineType::Footer then begin
DataExch."File Name" := CopyStr(Filename, 1, 250);
DataExch.Modify();
end;
// Now copy current file contents to table, also.
ExportGenericFixedWidth.SetDestination(OutStream);
ExportGenericFixedWidth.SetTableView(DataExchField);
ExportGenericFixedWidth.Export();
ExportFile.Close();
DataExchField.DeleteAll(true);
end;
end;
/// <summary>
/// Creates data exchange field records for flat file export based on record data and mapping configuration.
/// </summary>
/// <param name="DataExch">The data exchange record containing export configuration.</param>
/// <param name="LineNo">The line number in the export file.</param>
/// <param name="RecRef">Reference to the source record containing data to export.</param>
/// <remarks>
/// This procedure bridges the gap between source data records and the data exchange framework.
/// It identifies the appropriate table mapping and delegates field processing to handle data transformation.
/// </remarks>
[Scope('OnPrem')]
procedure InsertDataExchLineForFlatFile(var DataExch: Record "Data Exch."; LineNo: Integer; RecRef: RecordRef)
var
DataExchMapping: Record "Data Exch. Mapping";
TableID: Integer;
begin
DataExchMapping.Init();
DataExchMapping.SetRange("Data Exch. Def Code", DataExch."Data Exch. Def Code");
DataExchMapping.SetRange("Data Exch. Line Def Code", DataExch."Data Exch. Line Def Code");
if DataExchMapping.FindFirst() then begin
TableID := DataExchMapping."Table ID";
ProcessColumnMapping(DataExch, RecRef, LineNo, TableID);
end;
end;
/// <summary>
/// Processes column mapping by transforming source data according to field mapping rules and placing it in data exchange fields.
/// </summary>
/// <param name="DataExch">The data exchange record being processed.</param>
/// <param name="RecRef">Record reference containing the source data.</param>
/// <param name="LineNo">Line number in the data exchange structure.</param>
/// <param name="TableID">Table ID of the source record.</param>
local procedure ProcessColumnMapping(var DataExch: Record "Data Exch."; RecRef: RecordRef; LineNo: Integer; TableID: Integer)
var
DataExchDef: Record "Data Exch. Def";
DataExchColumnDef: Record "Data Exch. Column Def";
DataExchField: Record "Data Exch. Field";
DataExchFieldMapping: Record "Data Exch. Field Mapping";
TransformationRule: Record "Transformation Rule";
StringConversionManagement: Codeunit StringConversionManagement;
ValueAsDestType: Variant;
FieldRef: FieldRef;
ValueAsString: Text[250];
begin
if not DataExchDef.Get(DataExch."Data Exch. Def Code") then
Error(FormatNotDefinedErr, DataExch."Data Exch. Def Code");
PrepopulateColumns(DataExchDef, DataExch."Data Exch. Line Def Code", DataExch."Entry No.", LineNo);
DataExchFieldMapping.SetRange("Data Exch. Def Code", DataExchDef.Code);
DataExchFieldMapping.SetRange("Data Exch. Line Def Code", DataExch."Data Exch. Line Def Code");
DataExchFieldMapping.SetRange("Table ID", TableID);
DataExchFieldMapping.FindSet();
repeat
DataExchColumnDef.Get(DataExchDef.Code, DataExch."Data Exch. Line Def Code", DataExchFieldMapping."Column No.");
if DataExchFieldMapping."Use Default Value" then
ValueAsString := DataExchFieldMapping."Default Value"
else begin
FieldRef := RecRef.Field(DataExchFieldMapping."Field ID");
if FieldRef.Class = FieldClass::FlowField then
FieldRef.CalcField();
CheckOptional(DataExchFieldMapping.Optional, FieldRef);
CastToDestinationType(ValueAsDestType, FieldRef.Value, DataExchColumnDef, DataExchFieldMapping.Multiplier);
ValueAsString := FormatToText(ValueAsDestType, DataExchDef, DataExchColumnDef);
if TransformationRule.Get(DataExchFieldMapping."Transformation Rule") then
ValueAsString := CopyStr(TransformationRule.TransformText(ValueAsString), 1, DataExchColumnDef.Length);
if DataExchColumnDef."Text Padding Required" and (DataExchColumnDef."Pad Character" <> '') and (not DataExchColumnDef."Blank Zero") then
ValueAsString :=
StringConversionManagement.GetPaddedString(
ValueAsString,
DataExchColumnDef.Length,
DataExchColumnDef."Pad Character",
DataExchColumnDef.Justification);
end;
if DataExchDef."File Type" = DataExchDef."File Type"::"Fixed Text" then
ValueAsString := Format(ValueAsString, 0, StrSubstNo('<Text,%1>', DataExchColumnDef.Length));
CheckLength(ValueAsString, RecRef.Field(DataExchFieldMapping."Field ID"), DataExchDef, DataExchColumnDef);
DataExchField.Get(DataExch."Entry No.", LineNo, DataExchFieldMapping."Column No.");
DataExchField.Value := ValueAsString;
DataExchField.Modify();
until DataExchFieldMapping.Next() = 0;
end;
/// <summary>
/// Pre-populates data exchange fields with empty values based on the column definitions for the export format.
/// </summary>
/// <param name="DataExchDef">Data exchange definition containing the format structure.</param>
/// <param name="DataExchLineDefCode">Line definition code for the specific record type.</param>
/// <param name="DataExchEntryNo">Entry number of the data exchange record.</param>
/// <param name="DataExchLineNo">Line number within the data exchange entry.</param>
local procedure PrepopulateColumns(DataExchDef: Record "Data Exch. Def"; DataExchLineDefCode: Code[20]; DataExchEntryNo: Integer; DataExchLineNo: Integer)
var
DataExchField: Record "Data Exch. Field";
DataExchLineDef: Record "Data Exch. Line Def";
DataExchColumnDef: Record "Data Exch. Column Def";
ColumnIndex: Integer;
begin
if DataExchDef."File Type" in [DataExchDef."File Type"::"Fixed Text", DataExchDef."File Type"::Xml] then begin
DataExchColumnDef.SetRange("Data Exch. Def Code", DataExchDef.Code);
DataExchColumnDef.SetRange("Data Exch. Line Def Code", DataExchLineDefCode);
if not DataExchColumnDef.FindSet() then
Error(DataExchLineDefNotFoundErr, DataExchDef.Name, DataExchLineDefCode);
repeat
DataExchField.InsertRec(
DataExchEntryNo, DataExchLineNo, DataExchColumnDef."Column No.",
PadStr(DataExchColumnDef.Constant, DataExchColumnDef.Length), DataExchLineDefCode)
until DataExchColumnDef.Next() = 0;
end else begin
if not DataExchLineDef.Get(DataExchDef.Code, DataExchLineDefCode) then
Error(DataExchLineDefNotFoundErr, DataExchDef.Name, DataExchLineDefCode);
for ColumnIndex := 1 to DataExchLineDef."Column Count" do
if DataExchColumnDef.Get(DataExchDef.Code, DataExchLineDef.Code, ColumnIndex) then
DataExchField.InsertRec(
DataExchEntryNo, DataExchLineNo, ColumnIndex, DataExchColumnDef.Constant, DataExchLineDefCode)
else
DataExchField.InsertRec(DataExchEntryNo, DataExchLineNo, ColumnIndex, '', DataExchLineDefCode);
end;
end;
/// <summary>
/// Validates that optional fields can be empty while required fields must have values.
/// </summary>
/// <param name="Optional">Whether the field is marked as optional in the export definition.</param>
/// <param name="FieldRef">Reference to the field being validated.</param>
local procedure CheckOptional(Optional: Boolean; FieldRef: FieldRef)
var
Value: Variant;
StringValue: Text;
begin
if Optional then
exit;
Value := FieldRef.Value();
StringValue := Format(Value);
if ((Value.IsDecimal or Value.IsInteger or Value.IsBigInteger) and (StringValue = '0')) or
(StringValue = '')
then
FieldRef.TestField();
end;
/// <summary>
/// Converts source values to the appropriate destination data types based on column definitions.
/// </summary>
/// <param name="DestinationValue">The converted value in the target data type.</param>
/// <param name="SourceValue">The original value to be converted.</param>
/// <param name="DataExchColumnDef">Column definition containing data type and formatting rules.</param>
/// <param name="Multiplier">Numeric multiplier to apply to decimal values.</param>
local procedure CastToDestinationType(var DestinationValue: Variant; SourceValue: Variant; DataExchColumnDef: Record "Data Exch. Column Def"; Multiplier: Decimal)
var
ValueAsDecimal: Decimal;
ValueAsDate: Date;
ValueAsDateTime: DateTime;
ValueAsBoolean: Boolean;
IsHandled: Boolean;
begin
OnBeforeCastToDestinationType(DestinationValue, SourceValue, DataExchColumnDef, Multiplier, IsHandled);
if IsHandled then
exit;
case DataExchColumnDef."Data Type" of
DataExchColumnDef."Data Type"::Decimal:
begin
if Format(SourceValue) = '' then
ValueAsDecimal := 0
else
Evaluate(ValueAsDecimal, Format(SourceValue));
DestinationValue := Multiplier * ValueAsDecimal;
end;
DataExchColumnDef."Data Type"::Text:
DestinationValue := Format(SourceValue);
DataExchColumnDef."Data Type"::Date:
begin
Evaluate(ValueAsDate, Format(SourceValue));
DestinationValue := ValueAsDate;
end;
DataExchColumnDef."Data Type"::DateTime:
begin
if SourceValue.IsTime() then
SourceValue := CreateDateTime(Today(), SourceValue);
if SourceValue.IsDate() then
SourceValue := CreateDateTime(SourceValue, 0T);
Evaluate(ValueAsDateTime, Format(SourceValue, 0, 9), 9);
DestinationValue := ValueAsDateTime;
end;
DataExchColumnDef."Data Type"::Boolean:
begin
Evaluate(ValueAsBoolean, Format(SourceValue));
DestinationValue := ValueAsBoolean;
end;
end;
end;
/// <summary>
/// Formats values to text according to data exchange column definition settings and file format requirements.
/// </summary>
/// <param name="ValueToFormat">The value to be formatted as text.</param>
/// <param name="DataExchDef">Data exchange definition containing file format settings.</param>
/// <param name="DataExchColumnDef">Column definition with formatting rules and data format specifications.</param>
/// <returns>Formatted text value ready for export.</returns>
local procedure FormatToText(ValueToFormat: Variant; DataExchDef: Record "Data Exch. Def"; DataExchColumnDef: Record "Data Exch. Column Def"): Text[250]
begin
case true of
(Format(ValueToFormat) = '0') and (DataExchColumnDef."Blank Zero"):
exit('');
DataExchDef."File Type" = DataExchDef."File Type"::Xml:
exit(Format(ValueToFormat, 0, 9));
DataExchColumnDef."Data Format" <> '':
exit(Format(ValueToFormat, 0, DataExchColumnDef."Data Format"));
DataExchColumnDef."Data Type" = DataExchColumnDef."Data Type"::Decimal:
exit(Format(ValueToFormat, 0, '<Precision,2><Standard Format,2>')); // Format 2 always uses a period (.) as the decimal separator, regardless of the Regional setting.
else
exit(Format(ValueToFormat));
end;
end;
/// <summary>
/// Validates that field values meet the length requirements specified in the data exchange column definition.
/// </summary>
/// <param name="Value">The text value to validate.</param>
/// <param name="FieldRef">Reference to the source field for error reporting.</param>
/// <param name="DataExchDef">Data exchange definition containing validation rules.</param>
/// <param name="DataExchColumnDef">Column definition with length constraints.</param>
local procedure CheckLength(Value: Text; FieldRef: FieldRef; DataExchDef: Record "Data Exch. Def"; DataExchColumnDef: Record "Data Exch. Column Def")
var
DataExchDefCode: Code[20];
begin
DataExchDefCode := DataExchColumnDef."Data Exch. Def Code";
if (DataExchColumnDef.Length > 0) and (StrLen(Value) > DataExchColumnDef.Length) then
Error(IncorrectLengthOfValuesErr, GetType(DataExchDefCode), DataExchDefCode,
FieldRef.Caption, DataExchColumnDef.Length, StrLen(Value), Value);
if (DataExchDef."File Type" = DataExchDef."File Type"::"Fixed Text") and
(StrLen(Value) <> DataExchColumnDef.Length)
then
Error(IncorrectLengthOfValuesErr, GetType(DataExchDefCode), DataExchDefCode, FieldRef.Caption,
DataExchColumnDef.Length, StrLen(Value), Value);
end;
/// <summary>
/// Retrieves the type description for a data exchange definition.
/// </summary>
/// <param name="DataExchDefCode">Code of the data exchange definition.</param>
/// <returns>Type description as text.</returns>
local procedure GetType(DataExchDefCode: Code[20]): Text
var
DataExchDef: Record "Data Exch. Def";
begin
DataExchDef.Get(DataExchDefCode);
exit(Format(DataExchDef.Type));
end;
/// <summary>
/// Prepares positive pay header record with basic account and company information for export.
/// </summary>
/// <param name="DataExch">The data exchange record to associate with the header.</param>
/// <param name="BankAccountNo">The bank account number for the positive pay file.</param>
/// <remarks>
/// This procedure creates the header record that will be included in the positive pay export file.
/// The header contains essential identification information including company name, account number, and file date.
/// </remarks>
procedure PreparePosPayHeader(DataExch: Record "Data Exch."; BankAccountNo: Text[30])
var
PosPayHeader: Record "Positive Pay Header";
begin
PosPayHeader.Init();
PosPayHeader."Data Exch. Entry No." := DataExch."Entry No.";
PosPayHeader."Company Name" := CompanyName;
PosPayHeader."Account Number" := BankAccountNo;
PosPayHeader."Date of File" := Today;
PosPayHeader.Insert();
end;
/// <summary>
/// Prepares positive pay footer record with summary information for the export file.
/// </summary>
/// <param name="DataExch">The data exchange record to associate with the footer.</param>
/// <param name="DataExchDetalEntryNo">The detail entry number for cross-reference.</param>
/// <param name="BankAccountNo">The bank account number for the positive pay file.</param>
/// <remarks>
/// This procedure creates the footer record that concludes the positive pay export file.
/// The footer provides summary information and maintains referential integrity with detail records.
/// </remarks>
procedure PreparePosPayFooter(DataExch: Record "Data Exch."; DataExchDetalEntryNo: Integer; BankAccountNo: Text[30])
var
PosPayFooter: Record "Positive Pay Footer";
begin
PosPayFooter.Init();
PosPayFooter."Data Exch. Entry No." := DataExch."Entry No.";
PosPayFooter."Data Exch. Detail Entry No." := DataExchDetalEntryNo;
PosPayFooter."Account Number" := BankAccountNo;
PosPayFooter.Insert();
end;
/// <summary>
/// Integration event that allows customization of data type casting during export processing.
/// </summary>
/// <param name="DestinationValue">The converted value in the target data type.</param>
/// <param name="SourceValue">The original value from the source field.</param>
/// <param name="DataExchColumnDef">The column definition containing conversion rules.</param>
/// <param name="Multiplier">The multiplier to apply during numeric conversions.</param>
/// <param name="IsHandled">Indicates whether the conversion has been handled by subscriber code.</param>
/// <remarks>
/// This integration event enables customization of how field values are converted between different data types
/// during the export process. Subscribers can implement custom conversion logic for specific scenarios.
/// </remarks>
[IntegrationEvent(false, false)]
local procedure OnBeforeCastToDestinationType(var DestinationValue: Variant; SourceValue: Variant; DataExchColumnDef: Record "Data Exch. Column Def"; Multiplier: Decimal; var IsHandled: Boolean)
begin
end;
}