-
Notifications
You must be signed in to change notification settings - Fork 638
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[E-Document Connector] Logiq E-Document Connector #27562
Changes from all commits
dc10695
f2827a9
8bfe6c9
42f17bd
8169987
60be83e
c24ddcd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
{ | ||
"id": "f4a198ad-cd8c-44bb-aff1-814e0e28ab79", | ||
"name": "E-Document Connector - Logiq", | ||
"publisher": "Microsoft", | ||
"version": "26.0.0.0", | ||
"brief": "E-Document Connector - Logiq", | ||
"description": "E-Document Connector - Logiq", | ||
"privacyStatement": "https://go.microsoft.com/fwlink/?LinkId=724009", | ||
"EULA": "https://go.microsoft.com/fwlink/?linkid=2009120", | ||
"help": "https://go.microsoft.com/fwlink/?linkid=2204541", | ||
"url": "https://go.microsoft.com/fwlink/?LinkId=724011", | ||
"contextSensitiveHelpUrl": "https://go.microsoft.com/fwlink/?linkid=2206603", | ||
"logo": "ExtensionLogo.png", | ||
"dependencies": [ | ||
{ | ||
"id": "e1d97edc-c239-46b4-8d84-6368bdf67c8b", | ||
"name": "E-Document Core", | ||
"publisher": "Microsoft", | ||
"version": "26.0.0.0" | ||
} | ||
], | ||
"internalsVisibleTo": [ | ||
{ | ||
"id": "d57ff58b-78cc-488f-81d0-c42079507a2d", | ||
"name": "E-Document Connector - Logiq Tests", | ||
"publisher": "Microsoft" | ||
} | ||
], | ||
"screenshots": [], | ||
"platform": "26.0.0.0", | ||
"application": "26.0.0.0", | ||
"idRanges": [ | ||
{ | ||
"from": 6430, | ||
"to": 6439 | ||
} | ||
], | ||
"resourceExposurePolicy": { | ||
"allowDebugging": true, | ||
"allowDownloadingSource": true, | ||
"includeSourceInSymbolFile": true | ||
}, | ||
"features": [ | ||
"TranslationFile" | ||
], | ||
"target": "Cloud" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
// ------------------------------------------------------------------------------------------------ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. See License.txt in the project root for license information. | ||
// ------------------------------------------------------------------------------------------------ | ||
namespace Microsoft.EServices.EDocumentConnector.Logiq; | ||
|
||
enum 6430 "API Engine" | ||
{ | ||
Extensible = false; | ||
Access = Internal; | ||
|
||
value(0; " ") | ||
{ | ||
Caption = '', Locked = true; | ||
} | ||
value(1; Engine1) | ||
{ | ||
Caption = 'Engine 1'; | ||
} | ||
value(2; Engine3) | ||
{ | ||
Caption = 'Engine 3'; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,190 @@ | ||
// ------------------------------------------------------------------------------------------------ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. See License.txt in the project root for license information. | ||
// ------------------------------------------------------------------------------------------------ | ||
namespace Microsoft.EServices.EDocumentConnector.Logiq; | ||
codeunit 6430 Auth | ||
{ | ||
|
||
Permissions = | ||
tabledata "Connection Setup" = r, | ||
tabledata "Connection User Setup" = rm; | ||
|
||
internal procedure SetIsolatedStorageValue(var ValueKey: Guid; Value: SecretText; TokenDataScope: DataScope) | ||
begin | ||
if IsNullGuid(ValueKey) then | ||
ValueKey := CreateGuid(); | ||
|
||
IsolatedStorage.Set(ValueKey, Value, TokenDataScope); | ||
end; | ||
|
||
internal procedure SetIsolatedStorageValue(var ValueKey: Guid; Value: SecretText) | ||
begin | ||
this.SetIsolatedStorageValue(ValueKey, Value, DataScope::Company); | ||
end; | ||
|
||
internal procedure GetIsolatedStorageValue(var ValueKey: Guid; var Value: SecretText; TokenDataScope: DataScope) | ||
begin | ||
if IsNullGuid(ValueKey) then | ||
exit; | ||
IsolatedStorage.Get(ValueKey, TokenDataScope, Value); | ||
end; | ||
|
||
internal procedure GetIsolatedStorageValue(var ValueKey: Guid; var Value: SecretText) | ||
begin | ||
this.GetIsolatedStorageValue(ValueKey, Value, DataScope::Company); | ||
end; | ||
|
||
[NonDebuggable] | ||
internal procedure GetTokens() | ||
var | ||
LogiqConnectionSetup: Record "Connection Setup"; | ||
LogiqConnectionUserSetup: Record "Connection User Setup"; | ||
Client: HttpClient; | ||
Headers: HttpHeaders; | ||
Content: HttpContent; | ||
RequestMessage: HttpRequestMessage; | ||
ResponseMessage: HttpResponseMessage; | ||
AccessToken, RefreshToken : SecretText; | ||
AccessTokExpires, RefreshTokExpires : DateTime; | ||
begin | ||
this.CheckSetup(LogiqConnectionSetup); | ||
this.CheckUserCredentials(LogiqConnectionUserSetup); | ||
|
||
RequestMessage.Method('POST'); | ||
RequestMessage.SetRequestUri(LogiqConnectionSetup."Authentication URL"); | ||
|
||
this.BuildTokenRequestBody(Content); | ||
|
||
Content.GetHeaders(Headers); | ||
Headers.Clear(); | ||
Headers.Add('Content-Type', 'application/x-www-form-urlencoded'); | ||
|
||
RequestMessage.Content(Content); | ||
|
||
Client.Send(RequestMessage, ResponseMessage); | ||
|
||
if not ResponseMessage.IsSuccessStatusCode() then begin | ||
if GuiAllowed then | ||
Message(this.AuthenticationFailedErr); | ||
exit; | ||
end; | ||
|
||
this.ParseTokens(ResponseMessage, AccessToken, RefreshToken, AccessTokExpires, RefreshTokExpires); | ||
|
||
this.SaveTokens(AccessToken, RefreshToken, AccessTokExpires, RefreshTokExpires); | ||
end; | ||
|
||
local procedure BuildTokenRequestBody(var Content: HttpContent) | ||
var | ||
LogiqConnectionSetup: Record "Connection Setup"; | ||
LogiqConnectionUserSetup: Record "Connection User Setup"; | ||
BodyText: SecretText; | ||
begin | ||
LogiqConnectionSetup.Get(); | ||
LogiqConnectionUserSetup.Get(UserSecurityId()); | ||
|
||
if (not IsNullGuid(LogiqConnectionUserSetup."Refresh Token - Key")) and (LogiqConnectionUserSetup."Refresh Token Expiration" > (CurrentDateTime + 60 * 1000)) then | ||
BodyText := SecretText.SecretStrSubstNo(this.RefreshTokenBodyTok, LogiqConnectionSetup."Client ID", LogiqConnectionSetup.GetClientSecret(), LogiqConnectionUserSetup.GetRefreshToken()) | ||
else | ||
BodyText := SecretText.SecretStrSubstNo(this.CredentialsBodyTok, LogiqConnectionSetup."Client ID", LogiqConnectionSetup.GetClientSecret(), LogiqConnectionUserSetup.Username, LogiqConnectionUserSetup.GetPassword()); | ||
|
||
Content.WriteFrom(BodyText); | ||
end; | ||
|
||
internal procedure CheckUserCredentials(var LogiqConnectionUserSetup: Record "Connection User Setup") | ||
begin | ||
if not LogiqConnectionUserSetup.Get(UserSecurityId()) then | ||
Error(this.NoUserSetupErr); | ||
|
||
if (LogiqConnectionUserSetup.Username = '') or (IsNullGuid(LogiqConnectionUserSetup."Password - Key")) then | ||
Error(this.MissingCredentialsErr); | ||
end; | ||
|
||
internal procedure CheckUserSetup(var LogiqConnectionUserSetup: Record "Connection User Setup") | ||
var | ||
begin | ||
this.CheckUserCredentials(LogiqConnectionUserSetup); | ||
|
||
if (LogiqConnectionUserSetup."API Engine" = LogiqConnectionUserSetup."API Engine"::" ") then | ||
Error(this.MissingAPIEngineErr); | ||
|
||
if (LogiqConnectionUserSetup."Document Transfer Endpoint" = '') or (LogiqConnectionUserSetup."Document Status Endpoint" = '') then | ||
Error(this.MissingEndpointsErr); | ||
end; | ||
|
||
internal procedure CheckSetup(var LogiqConnectionSetup: Record "Connection Setup") | ||
begin | ||
if not LogiqConnectionSetup.Get() then | ||
Error(this.NoSetupErr); | ||
|
||
if (LogiqConnectionSetup."Client ID" = '') or (IsNullGuid(LogiqConnectionSetup."Client Secret")) then | ||
Error(this.MissingClientInfoErr); | ||
|
||
if LogiqConnectionSetup."Authentication URL" = '' then | ||
Error(this.MissingAuthUrlErr); | ||
|
||
if LogiqConnectionSetup."Base URL" = '' then | ||
Error(this.MissingBaseUrlErr); | ||
end; | ||
|
||
[NonDebuggable] | ||
local procedure ParseTokens(ResponseMessage: HttpResponseMessage; var AccessToken: SecretText; var RefreshToken: SecretText; var AccessTokExpires: DateTime; var RefreshTokExpires: DateTime) | ||
var | ||
ContentJson: JsonObject; | ||
JsonTok: JsonToken; | ||
ResponseTxt: Text; | ||
begin | ||
ResponseMessage.Content.ReadAs(ResponseTxt); | ||
ContentJson.ReadFrom(ResponseTxt); | ||
if ContentJson.Get('access_token', JsonTok) then | ||
AccessToken := JsonTok.AsValue().AsText(); | ||
if ContentJson.Get('refresh_token', JsonTok) then | ||
RefreshToken := JsonTok.AsValue().AsText(); | ||
if ContentJson.Get('expires_in', JsonTok) then | ||
AccessTokExpires := CurrentDateTime + JsonTok.AsValue().AsInteger() * 1000; | ||
if ContentJson.Get('refresh_expires_in', JsonTok) then | ||
RefreshTokExpires := CurrentDateTime + JsonTok.AsValue().AsInteger() * 1000; | ||
end; | ||
|
||
local procedure SaveTokens(AccessToken: SecretText; RefreshToken: SecretText; AccessTokExpires: DateTime; RefreshTokExpires: DateTime) | ||
var | ||
LogiqConnectionUserSetup: Record "Connection User Setup"; | ||
begin | ||
LogiqConnectionUserSetup.Get(UserSecurityId()); | ||
this.SetIsolatedStorageValue(LogiqConnectionUserSetup."Access Token - Key", AccessToken, DataScope::User); | ||
this.SetIsolatedStorageValue(LogiqConnectionUserSetup."Refresh Token - Key", RefreshToken, DataScope::User); | ||
LogiqConnectionUserSetup."Access Token Expiration" := AccessTokExpires; | ||
LogiqConnectionUserSetup."Refresh Token Expiration" := RefreshTokExpires; | ||
LogiqConnectionUserSetup.Modify(false); | ||
end; | ||
|
||
internal procedure HasToken(ValueKey: Guid; DataScope: DataScope): Boolean | ||
begin | ||
exit(IsolatedStorage.Contains(ValueKey, DataScope)); | ||
end; | ||
|
||
internal procedure CheckUpdateTokens() | ||
var | ||
LogiqConnectionUserSetup: Record "Connection User Setup"; | ||
begin | ||
if not LogiqConnectionUserSetup.Get(UserSecurityId()) then | ||
Error(this.NoUserSetupErr); | ||
if IsNullGuid(LogiqConnectionUserSetup."Access Token - Key") or (LogiqConnectionUserSetup."Access Token Expiration" < (CurrentDateTime + 5 * 60 * 1000)) then | ||
this.GetTokens(); | ||
end; | ||
|
||
var | ||
AuthenticationFailedErr: Label 'Logiq authentication failed. Please check the user credentials.'; | ||
CredentialsBodyTok: Label 'grant_type=password&scope=openid&client_id=%1&client_secret=%2&username=%3&password=%4', Locked = true; | ||
MissingAPIEngineErr: Label 'API Engine is missing. Please select the API Engine in the Logiq Connection User Setup page.'; | ||
MissingAuthUrlErr: Label 'Authentication URL is missing. Please fill the Authentication URL in the Logiq Connection Setup page.'; | ||
MissingBaseUrlErr: Label 'Base URL is missing. Please fill the API Base URL in the Logiq Connection Setup page.'; | ||
MissingClientInfoErr: Label 'Client ID or Client Secret is missing. Please fill the Client ID and Client Secret in the Logiq Connection Setup page.'; | ||
MissingCredentialsErr: Label 'User credentials are missing. Please enter username and password in the Logiq Connection User Setup page.'; | ||
MissingEndpointsErr: Label 'Endpoints are missing. Please fill the Document Transfer Endpoint and Document Status Endpoint in the Logiq Connection User Setup page.'; | ||
NoSetupErr: Label 'No setup found. Please fill the setup in the Logiq Connection Setup page.'; | ||
NoUserSetupErr: Label 'No user setup found. Please fill the user setup in the Logiq Connection User Setup page.'; | ||
RefreshTokenBodyTok: Label 'grant_type=refresh_token&client_id=%1&client_secret=%2&refresh_token=%3', Locked = true; | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
// ------------------------------------------------------------------------------------------------ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. See License.txt in the project root for license information. | ||
// ------------------------------------------------------------------------------------------------ | ||
namespace Microsoft.EServices.EDocumentConnector.Logiq; | ||
|
||
page 6430 "Connection Setup" | ||
{ | ||
Caption = 'Logiq Connection Setup'; | ||
PageType = Card; | ||
ApplicationArea = Basic, Suite; | ||
UsageCategory = None; | ||
SourceTable = "Connection Setup"; | ||
|
||
layout | ||
{ | ||
area(Content) | ||
{ | ||
group(General) | ||
{ | ||
Caption = 'General'; | ||
|
||
field(ClientID; Rec."Client ID") | ||
{ | ||
ShowMandatory = true; | ||
} | ||
field(ClientSecret; this.ClientSecret) | ||
{ | ||
Caption = 'Client Secret'; | ||
ToolTip = 'Specifies the client secret token.'; | ||
ExtendedDatatype = Masked; | ||
ShowMandatory = true; | ||
|
||
trigger OnValidate() | ||
begin | ||
this.LogiqAuth.SetIsolatedStorageValue(Rec."Client Secret", this.ClientSecret); | ||
end; | ||
} | ||
field("Authentication URL"; Rec."Authentication URL") | ||
{ | ||
} | ||
field("Base URL"; Rec."Base URL") | ||
{ | ||
} | ||
field("File List Endpoint"; Rec."File List Endpoint") | ||
{ | ||
} | ||
} | ||
} | ||
} | ||
|
||
actions | ||
{ | ||
area(Processing) | ||
{ | ||
action(Connect) | ||
{ | ||
ApplicationArea = All; | ||
Caption = 'User Setup'; | ||
Image = Setup; | ||
ToolTip = 'Open page for User Setup.'; | ||
|
||
trigger OnAction() | ||
var | ||
LogiqConnectionUserSetup: Record "Connection User Setup"; | ||
LoqiqConnectionUserSetupPage: Page "Connection User Setup"; | ||
begin | ||
LogiqConnectionUserSetup.FindUserSetup(UserSecurityId()); | ||
LoqiqConnectionUserSetupPage.SetRecord(LogiqConnectionUserSetup); | ||
LoqiqConnectionUserSetupPage.Run(); | ||
end; | ||
} | ||
} | ||
area(Promoted) | ||
{ | ||
actionref(Connect_Promoted; Connect) | ||
{ | ||
} | ||
} | ||
} | ||
|
||
var | ||
LogiqAuth: Codeunit Auth; | ||
[NonDebuggable] | ||
ClientSecret: Text; | ||
|
||
trigger OnOpenPage() | ||
begin | ||
if not Rec.Get() then begin | ||
Rec.Init(); | ||
Rec.Insert(true); | ||
end; | ||
|
||
if this.LogiqAuth.HasToken(Rec."Client Secret", DataScope::Company) then | ||
this.ClientSecret := '*'; | ||
end; | ||
} |
Unchanged files with check annotations Beta
{ | ||
Caption = ' '; | ||
InstructionalText = 'Select which bank accounts to set up.'; | ||
field("No."; "No.") | ||
Check warning on line 28 in Apps/W1/AMCBanking365Fundamentals/app/Pages/AMCBankAssistBankAccount.Page.al
|
||
{ | ||
ApplicationArea = Basic, Suite; | ||
Editable = false; | ||
ToolTip = 'Specifies the number of the involved entry or record, according to the specified number series.'; | ||
} | ||
field(Name; Name) | ||
Check warning on line 34 in Apps/W1/AMCBanking365Fundamentals/app/Pages/AMCBankAssistBankAccount.Page.al
|
||
{ | ||
ApplicationArea = Basic, Suite; | ||
Editable = false; | ||
ToolTip = 'Specifies the name of the bank where you have the bank account.'; | ||
} | ||
field(CurrencyCode; "Currency Code") | ||
Check warning on line 40 in Apps/W1/AMCBanking365Fundamentals/app/Pages/AMCBankAssistBankAccount.Page.al
|
||
{ | ||
ApplicationArea = Basic, Suite; | ||
Editable = false; | ||
Caption = 'Currency Code'; | ||
ToolTip = 'Specifies the relevant currency code for the bank account.'; | ||
} | ||
field("Bank Account No."; "Bank Account No.") | ||
Check warning on line 47 in Apps/W1/AMCBanking365Fundamentals/app/Pages/AMCBankAssistBankAccount.Page.al
|
||
{ | ||
ApplicationArea = Basic, Suite; | ||
Editable = false; | ||
ToolTip = 'Specifies the number used by the bank for the bank account.'; | ||
} | ||
field(Chose; "Automatic Logon Possible") | ||
Check warning on line 53 in Apps/W1/AMCBanking365Fundamentals/app/Pages/AMCBankAssistBankAccount.Page.al
|
||
{ | ||
ApplicationArea = Basic, Suite; | ||
Editable = true; |
{ | ||
addAfter("Creditor No.") | ||
{ | ||
field("Bank Name format"; "AMC Bank Name") | ||
Check warning on line 17 in Apps/W1/AMCBanking365Fundamentals/app/Pages/AMCBankBankAccountCard.PageExt.al
|
||
{ | ||
ApplicationArea = Basic, Suite; | ||
Importance = Additional; | ||
} | ||
addafter("Payment Export Format") | ||
{ | ||
field("Bank Filename"; "AMC Bank File Name") | ||
Check warning on line 27 in Apps/W1/AMCBanking365Fundamentals/app/Pages/AMCBankBankAccountCard.PageExt.al
|
||
{ | ||
ApplicationArea = Basic, Suite; | ||
Importance = Additional; |
{ | ||
addlast(Group) | ||
{ | ||
field("XTL Journal"; "AMC Bank XTL Journal") | ||
Check warning on line 13 in Apps/W1/AMCBanking365Fundamentals/app/Pages/AMCBankCreditTrfRegExt.PageExt.al
|
||
{ | ||
ApplicationArea = Basic, Suite; | ||
Importance = Additional; |
group(User) | ||
{ | ||
Caption = 'User information'; | ||
field("User Name"; "User Name") | ||
Check warning on line 46 in Apps/W1/AMCBanking365Fundamentals/app/Pages/AMCBankingSetup.Page.al
|
||
{ | ||
ApplicationArea = Basic, Suite; | ||
ShowMandatory = true; | ||
group(SolutionGrp) | ||
{ | ||
Caption = ''; | ||
field("Solution"; "Solution") | ||
Check warning on line 77 in Apps/W1/AMCBanking365Fundamentals/app/Pages/AMCBankingSetup.Page.al
|
||
{ | ||
ApplicationArea = Suite; | ||
Visible = true; |
local procedure CreateDataExchDef(var DataExchDef: Record "Data Exch. Def"; ProcessingXMLport: Integer; ColumnSeparator: Option; FileType: Option) | ||
begin | ||
DataExchDef.InsertRecForExport(LibraryUtility.GenerateRandomCode(DataExchDef.FieldNo(Code), DATABASE::"Data Exch. Def"), | ||
LibraryUtility.GenerateGUID(), DataExchDef.Type::"Payment Export", ProcessingXMLport, FileType); | ||
Check warning on line 66 in Apps/W1/AMCBanking365Fundamentals/test/src/Bank/PaymentExportXMLPort.Codeunit.al
|
||
DataExchDef."Column Separator" := ColumnSeparator; | ||
DataExchDef."File Encoding" := DataExchDef."File Encoding"::WINDOWS; | ||
DataExchDef.Modify(); |
SetGenJnlLine(GenJournalLine); | ||
SetTableView(Vendor); | ||
InitializeRequest(WorkDate(), false, 0, false, WorkDate(), LibraryUtility.GenerateGUID(), false, | ||
GenJournalBatch."Bal. Account Type", GenJournalBatch."Bal. Account No.", 0); | ||
Check warning on line 436 in Apps/W1/AMCBanking365Fundamentals/test/src/Bank/PmtExportAMCExtractData.Codeunit.al
|
||
UseRequestPage(false); | ||
RunModal(); | ||
end; |
SetGenJnlLine(GenJournalLine); | ||
SetTableView(Vendor); | ||
InitializeRequest(WorkDate(), false, 0, SkipExportedPayments, WorkDate(), LibraryUtility.GenerateGUID(), false, | ||
GenJournalBatch."Bal. Account Type", GenJournalBatch."Bal. Account No.", 0); | ||
Check warning on line 691 in Apps/W1/AMCBanking365Fundamentals/test/src/Bank/PmtExportAMCTestFormat.Codeunit.al
|
||
UseRequestPage(false); | ||
RunModal(); | ||
end; |
VerifyPropertyInJSON(CompanyInformationJSON, 'phoneNumber', CompanyInformation."Phone No."); | ||
VerifyPropertyInJSON(CompanyInformationJSON, 'faxNumber', CompanyInformation."Fax No."); | ||
VerifyPropertyInJSON(CompanyInformationJSON, 'email', CompanyInformation."E-Mail"); | ||
VerifyPropertyInJSON(CompanyInformationJSON, 'website', CompanyInformation."Home Page"); | ||
Check warning on line 154 in Apps/W1/APIV1/test/src/APIV1CompanyInfoE2E.Codeunit.al
|
||
VerifyPropertyInJSON(CompanyInformationJSON, 'taxRegistrationNumber', CompanyInformation."VAT Registration No."); | ||
VerifyPropertyInJSON(CompanyInformationJSON, 'industry', CompanyInformation."Industrial Classification"); | ||
VerifyPropertyInJSON(CompanyInformationJSON, 'currencyCode', GeneralLedgerSetup."LCY Code"); |
Assert.AreEqual('123456789', Customer."Phone No.", 'Customer should have the correct phone number.'); | ||
Assert.AreEqual('a@b.com', Customer."E-Mail", 'Customer should have the correct email.'); | ||
Assert.AreEqual('microsoft.com', Customer."Home Page", 'Customer should have the correct website.'); | ||
Check warning on line 440 in Apps/W1/APIV1/test/src/APIV1CustomersE2E.Codeunit.al
|
||
Assert.AreEqual(TRUE, Customer."Tax Liable", 'Customer should have the correct ''tax liable'' information.'); | ||
Assert.AreEqual(TaxAreaID, Customer."Tax Area ID", 'Customer should have the correct tax area id.'); | ||
Assert.AreEqual(Currency.Code, Customer."Currency Code", 'Customer should have the correct currency code.'); |
Check warning on line 1 in Apps/W1/APIV1/test/src/TODO - Tests.txt
|
||
Employees Page + Tests should be added | ||
Time Registration Entries should be tested differently | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All files should contain the license comments as the first thing.