unit qbAPI;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, AdvUtil, Data.DB, Vcl.Grids, AdvObj,
  BaseGrid, AdvGrid, DBAdvGrid, MemDS, DBAccess, Uni, Vcl.StdCtrls, Vcl.Mask,
  vcl.wwdbedit, vcl.wwdotdot, vcl.wwdbcomb, REST.Client, REST.Types, System.JSON,
  System.Generics.Collections, AdvEdit, vcl.wwdblook, vcl.wwdbdatetimepicker,
  System.Hash, Api.Database, Vcl.ExtCtrls, WEBLib.Forms, WEBLib.Controls, WEBLib.StdCtrls,
  WEBLib.ExtCtrls, WEBLib.REST, WEBLib.WebTools,System.Net.HttpClient,
  System.Net.URLClient, System.Net.HttpClientComponent, System.netencoding,
  IdHTTP, IdSSLOpenSSL, IdSSLOpenSSLHeaders, System.DateUtils, System.IniFiles;


type
  TfQB = class(TForm)
    Memo1: TMemo;
    Button1: TButton;
    Memo2: TMemo;
    Button2: TButton;
    asgData: TAdvStringGrid;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    { Private declarations }
     httpReqTokenRefresh: TWebHttpRequest;

     var
      AccessToken,RefreshToken,CompanyID,Client,Secret: string;
      LastRefresh: TDateTime;
  public
    { Public declarations }
    procedure getCompanyInfo();
    procedure LoadJsonArray(jaData: TJSONArray);
    function RefreshAccessToken(): string;
    procedure ConfigureSSL(IOHandler: TIdSSLIOHandlerSocketOpenSSL);
    procedure SaveTokens(AccessToken, RefreshToken: string);
    procedure getCustomers();
  end;

var
  fQB: TfQB;

implementation

uses
  Common.Logging;

{$R *.dfm}

procedure TfQB.Button1Click(Sender: TObject);
begin
  getCompanyInfo();
end;

procedure TfQB.SaveTokens(AccessToken, RefreshToken: string);
var
  f: TStringList;
  iniStr, line: string;
  iniFile: TIniFile;
begin
  iniFile := TIniFile.Create( ExtractFilePath(Application.ExeName) + 'kgOrdersServer.ini' );
  try
    iniFile.WriteString('Quickbooks', 'RefreshToken', RefreshToken);
    LastRefresh := Now;
    Logger.Log(1, 'Tokens Successfully Saved');

  finally
    IniFile.Free;
  end;
  f := TStringList.Create;


    // Save to file (overwrites existing file)
  f.SaveToFile('QB.txt');
  f.Free;
end;


procedure TfQB.Button2Click(Sender: TObject);
begin
  GetCustomers();
end;

procedure TfQB.getCustomers();
var
  restClient: TRESTClient;
  restRequest: TRESTRequest;
  restResponse: TRESTResponse;
  param: TRESTRequestParameter;
  res: string;
  jsValue: TJSONValue;
  Customer: TJSONValue;
  jsObj: TJSONObject;
  CustomerList: TJSONArray;
  pair: TJSONPair;
begin
  restClient := TRESTClient.Create(nil);
  restClient.BaseURL := 'https://sandbox-quickbooks.api.intuit.com';

  restRequest := TRESTRequest.Create(nil);
  restRequest.Client := restClient;

  restResponse := TRESTResponse.Create(nil);
  restRequest.Response := restResponse;


  if MinutesBetween(Now, LastRefresh) > 58  then
  begin
    RefreshAccessToken();
  end;

  restRequest.Method := rmGET;
  res := '/v3/company/' + companyID + '/query?query=select * from Customer&minorversion=75';
  restRequest.Resource := res;

  param := restRequest.Params.AddItem;
  param.Name := 'Authorization';
  param.Kind := pkHTTPHEADER;
  param.Options := param.Options + [TRESTRequestParameterOption.poDoNotEncode];
  param.Value := 'Bearer ' + AccessToken;

  restRequest.Execute;

  memo1.Lines.Add(restresponse.Content);

  jsValue := restResponse.JSONValue;

  jsObj := TJSONObject(jsValue);
  Memo2.Lines.Add( jsObj.Format(2) );
  //CustomerList := TJSONArray(restResponse.JSONValue);

  CustomerList := TJSONArray( TJSONObject( jsObj.GetValue('QueryResponse') ).GetValue('Customer')) ;

  LoadJSONArray( CustomerList );

  {for Customer in CustomerList do
  begin
    for pair in TJSONObject(Customer).pair do
    begin

    end;
  end;  }

  //jsObj := TJSONObject.ParseJSONValue(restresponse.Content) as TJSONObject;
  //companyInfo := TJSONObject(jsObj.GetValue('CompanyInfo'));

  restClient.Free;
  restRequest.Free;
  restResponse.Free;

end;

procedure TfQB.ConfigureSSL(IOHandler: TIdSSLIOHandlerSocketOpenSSL);
begin
  // For Indy 10.6.2+ (Delphi 10.2 Tokyo+)
  IOHandler.SSLOptions.Method := sslvTLSv1_2;

  // Set SSL versions - maximum compatibility
  IOHandler.SSLOptions.SSLVersions := [sslvTLSv1_2];

  // For very old Indy versions (fallback)
  if not (sslvTLSv1_2 in IOHandler.SSLOptions.SSLVersions) then
  begin
    IOHandler.SSLOptions.Method := sslvSSLv23;
    IOHandler.SSLOptions.SSLVersions := [sslvTLSv1, sslvTLSv1_1, sslvTLSv1_2];
  end;

  IOHandler.SSLOptions.Mode := sslmClient;
end;

procedure TfQB.FormCreate(Sender: TObject);
var
  iniFile: TIniFile;
begin
  iniFile := TIniFile.Create( ExtractFilePath(Application.ExeName) + 'kgOrdersServer.ini' );
  Client := iniFile.ReadString('Quickbooks', 'ClientID', '');
  Secret := iniFile.ReadString('Quickbooks', 'ClientSecret', '');
  CompanyID := iniFile.ReadString('Quickbooks', 'CompanyID', '');
  RefreshToken := iniFile.ReadString('Quickbooks', 'RefreshToken', '');
end;

function TfQB.RefreshAccessToken: string;
// Refresh Token changes so make sure to save refresh token.
var
  IdHTTP: TIdHTTP;
  SSLIO: TIdSSLIOHandlerSocketOpenSSL;
  RequestStream: TStringStream;
  EncodedAuth, EncodedAuth2, PostData, response: string;
  f: TStringList;
  fi: string;
  JSObj: TJSONObject;
  iniFile: TIniFile;
  Encoder: TBase64Encoding;
begin
  // 1. Encode credentials (same as working Postman request)

  // TNetEncoding.Base64.Encode adds a new line every 72 chars, this stops that
  Encoder := TBase64Encoding.Create(0);

  if( (Client = '') or (Secret = '') ) then
  begin
    Logger.Log(1, 'Missing Client ID or Client Secret in INI File');
    Exit();
  end;

  EncodedAuth := Encoder.Encode(Client + ':' + Secret);
  Memo1.Lines.Add(EncodedAuth);
  if RefreshToken = '' then
  begin
    Logger.Log(3, 'Missing Refresh Token, Please Manually Get a New One and Store in INI File');
    Exit();
  end;

  // 2. Prepare POST data (EXACTLY as in Postman)
  PostData := 'grant_type=refresh_token&refresh_token=' + RefreshToken;

  // 3. Configure HTTP client
  IdHTTP := TIdHTTP.Create(nil);
  SSLIO := TIdSSLIOHandlerSocketOpenSSL.Create(nil);
  try
    // Force TLS 1.2
    SSLIO.SSLOptions.Method := sslvTLSv1_2;
    SSLIO.SSLOptions.SSLVersions := [sslvTLSv1_2];
    IdHTTP.IOHandler := SSLIO;

    // Set headers (EXACT match with Postman)
    IdHTTP.Request.ContentType := 'application/x-www-form-urlencoded';
    IdHTTP.Request.Accept := 'application/json';
    IdHTTP.Request.CustomHeaders.AddValue('Authorization', 'Basic ' + EncodedAuth);

    // 4. Create and send request
    RequestStream := TStringStream.Create(PostData, TEncoding.UTF8);
    try

      // Execute POST
      try
        response := IdHTTP.Post('https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer', RequestStream);
        JSObj :=  TJSONObject.ParseJSONValue(response) as TJSONObject;
        RefreshToken := JSObj.GetValue('refresh_token').ToString.Trim(['"']);
        AccessToken := JSObj.GetValue('access_token').ToString.Trim(['"']);
        SaveTokens(AccessToken, RefreshToken);
        Result := AccessToken;
        Logger.Log(1, 'qbAPI - Tokens Successfully Saved');
        Memo1.Lines.Add('Tokens Successfully Saved');
      except
        on E: EIdHTTPProtocolException do
          Memo2.Lines.Add('Error: ' + E.Message + #13#10 + 'Response: ' + E.ErrorMessage);
      end;
    finally
      RequestStream.Free;
    end;
  finally
    SSLIO.Free;
    IdHTTP.Free;
  end;
end;

procedure TfQB.getCompanyInfo();
var
  restClient: TRESTClient;
  restRequest: TRESTRequest;
  restResponse: TRESTResponse;
  param: TRESTRequestParameter;
  res: string;
  jsValue: TJSONValue;
  jsObj, companyInfo: TJSONObject;
begin
  restClient := TRESTClient.Create(nil);
  restClient.BaseURL := 'https://sandbox-quickbooks.api.intuit.com';

  restRequest := TRESTRequest.Create(nil);
  restRequest.Client := restClient;

  restResponse := TRESTResponse.Create(nil);
  restRequest.Response := restResponse;


  if MinutesBetween(Now, LastRefresh) > 58  then
  begin
    RefreshAccessToken();
  end;
  restRequest.Method := rmGET;
  res := '/v3/company/' + companyID + '/companyinfo/' + companyID;
  restRequest.Resource := res;

  param := restRequest.Params.AddItem;
  param.Name := 'Authorization';
  param.Kind := pkHTTPHEADER;
  param.Options := param.Options + [TRESTRequestParameterOption.poDoNotEncode];
  param.Value := 'Bearer ' + AccessToken;

  restRequest.Execute;

  memo1.Lines.Add(restresponse.Content) ;
  //jsValue := restResponse.JSONValue;
  //jsObj := TJSONObject.ParseJSONValue(restresponse.Content) as TJSONObject;
  //companyInfo := TJSONObject(jsObj.GetValue('CompanyInfo'));

  restClient.Free;
  restRequest.Free;
  restResponse.Free;

end;

procedure TfQB.LoadJsonArray(jaData: TJSONArray);
var
  jso: TJSONObject;
  i, j: integer;
  row: integer;
begin
  Memo1.Lines.Add( '---------------------------------------------------------------' );
  Memo1.Lines.Add( 'LoadJsonArray into asgData' );

  asgData.ClearAll;
  asgData.RowCount := 1;
  asgData.StartUpdate;

  jso := TJSONObject(jaData.Items[0]);
  asgData.ColCount := jso.Count;
  for i := 0 to jso.Count - 1 do
    asgData.Cells[i+1, 0] := jso.Pairs[i].JsonString.Value;

  for i := 0 to jaData.Count - 1 do
  begin
    jso := TJSONObject(jaData.Items[i]);
    asgData.RowCount := asgData.RowCount + 1;
    row := asgData.RowCount - 1;
    for j := 0 to jso.Count - 1 do
      asgData.Cells[j+1, row] := jso.Pairs[j].JsonValue.Value;
  end;
  asgData.EndUpdate;
  asgData.AutoSizeColumns(true);
end;


end.
