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;
    Button3: TButton;
    Button4: TButton;
    Button5: TButton;
    uq: TUniQuery;
    uqORDER_ID: TIntegerField;
    uqCOMPANY_ID: TIntegerField;
    uqUSER_ID: TIntegerField;
    uqORDER_DATE: TDateTimeField;
    uqSTART_DATE: TDateField;
    uqEND_DATE: TDateField;
    uqORDER_STATUS: TStringField;
    uqSCHED_JSON: TStringField;
    uqstaff_fields_order_date: TDateField;
    uqstaff_fields_proof_date: TDateField;
    uqstaff_fields_ship_date: TDateField;
    uqstaff_fields_ship_via: TStringField;
    uqstaff_fields_price: TStringField;
    uqstaff_fields_invoice_to: TStringField;
    uqstaff_fields_invoice_attention: TStringField;
    uqstaff_fields_ship_to: TStringField;
    uqstaff_fields_ship_attention: TStringField;
    uqstaff_fields_po_number: TStringField;
    uqstaff_fields_job_name: TStringField;
    uqstaff_fields_art_due: TDateField;
    uqstaff_fields_plate_due: TDateField;
    uqplates_job_number: TStringField;
    uqsupplied_by_customer_b_w_or_co: TStringField;
    uqsupplied_by_customer_plates: TStringField;
    uqsupplied_by_customer_sample: TStringField;
    uqsupplied_by_customer_dimension: TStringField;
    uqsupplied_by_customer_other: TStringField;
    uqsupplied_by_customer_disk: TStringField;
    uqsupplied_by_customer_e_mail: TStringField;
    uqsupplied_by_customer_ftp: TStringField;
    uqplates_plate_material: TStringField;
    uqplates_thickness: TStringField;
    uqsupplied_by_customer_total_inc: TStringField;
    uqsupplied_by_customer_sheets_us: TStringField;
    uqsupplied_by_customer_initials: TStringField;
    uqproofing_pdf: TStringField;
    uqproofing_pdf_to: TStringField;
    uqproofing_pdf_date_1: TDateField;
    uqproofing_pdf_date_2: TDateField;
    uqproofing_pdf_date_3: TDateField;
    uqproofing_full_size_ink_jet_for: TStringField;
    uqproofing_ink_jet_to: TStringField;
    uqproofing_ink_jet_to_2: TStringField;
    uqproofing_ink_jet_date_1: TDateField;
    uqproofing_ink_jet_date_2: TDateField;
    uqproofing_ink_jet_date_3: TDateField;
    uqproofing_color_contract: TStringField;
    uqproofing_color_contrac_to: TStringField;
    uqproofing_color_contrac_date_1: TDateField;
    uqproofing_color_contrac_date_2: TDateField;
    uqproofing_digital_color_key: TStringField;
    uqproofing_digital_color_to: TStringField;
    uqproofing_digital_color_date_1: TDateField;
    uqquantity_and_colors_press_name: TStringField;
    uqquantity_and_colors_anilox_info: TStringField;
    uqplate_marks_microdots: TStringField;
    uqplate_marks_microdots_comments: TStringField;
    uqplate_marks_crosshairs: TStringField;
    uqplate_marks_crosshairs_comments: TStringField;
    uqplate_marks_color_bars: TStringField;
    uqplate_marks_color_bars_comments: TStringField;
    uqplate_marks_other: TStringField;
    uqplate_marks_other_comments: TStringField;
    uqprint_orientation_print_orient: TStringField;
    uqlayout_around: TStringField;
    uqlayout_accross: TStringField;
    uqlayout_surface_print: TStringField;
    uqlayout_reverse_print: TStringField;
    uqlayout_cylinder_repeat: TStringField;
    uqlayout_cutoff_dimension: TStringField;
    uqlayout_pitch: TStringField;
    uqlayout_teeth: TStringField;
    uqlayout_bleed: TStringField;
    uqlayout_cutback: TStringField;
    uqlayout_minimum_trap_dim: TStringField;
    uqlayout_maximum_trap_dim: TStringField;
    uqupc_size: TStringField;
    uqupc_bar_width_reduction: TStringField;
    uqquantity_and_colors_qty_colors: TStringField;
    uqgeneral_comments: TStringField;
    uqstaff_fields_quickbooks_item: TStringField;
    uqstaff_fields_quantity: TStringField;
    uqupc_distortion_percent: TStringField;
    uqupc_distortion_amount: TStringField;
    uqstaff_fields_art_location: TStringField;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
    procedure Button4Click(Sender: TObject);
    procedure Button5Click(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    { Private declarations }
    strict private
    ordersDB: TApiDatabase;
     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, uLibrary;

{$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.Button3Click(Sender: TObject);
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;
  //GET /v3/company/<realmId>/customer/<customerId>
  res := '/v3/company/' + companyid + '/customer/58';
  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(res);
  Memo1.Lines.Add( jsObj.Format(2) );

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

 // LoadJSONArray( CustomerList );

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

end;

procedure TfQB.Button4Click(Sender: TObject);
var
  restClient: TRESTClient;
  restRequest: TRESTRequest;
  restResponse: TRESTResponse;
  param: TRESTRequestParameter;
  res: string;
  jsValue: TJSONValue;
  Customer: TJSONValue;
  jsObj: TJSONObject;
  CustomerList: TJSONArray;
  pair: TJSONPair;
  estimateJSON: TJSONObject;
begin
  ///v3/company/<realmID>/estimate
  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 := rmPOST;
  res := '/v3/company/' + companyID + '/estimate';
  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(res);
  Memo1.Lines.Add( jsObj.Format(2) );
  //CustomerList := TJSONArray(restResponse.JSONValue);

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

 // LoadJSONArray( CustomerList );

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

procedure TfQB.Button5Click(Sender: TObject);
var
  SQL: string;
  count: integer;
begin
  SQL := 'SELECT c.CUSTOMER_ID, c.SHORT_NAME, c.NAME FROM customers c ' +
          'WHERE EXISTS (SELECT 1 FROM corrugated_plate_orders cpo WHERE cpo.COMPANY_ID = c.CUSTOMER_ID) '+
          'OR EXISTS (SELECT 1 FROM web_plate_orders wpo WHERE wpo.COMPANY_ID = c.CUSTOMER_ID) ORDER BY c.SHORT_NAME';
  doQuery(ordersDB.UniQuery1, SQL);

  while not ordersDB.UniQuery1.Eof do
  begin
    Memo2.Lines.Add(ordersDB.UniQuery1.FieldByName('CUSTOMER_ID').AsString + ', ' + ordersDB.UniQuery1.FieldByName('SHORT_NAME').AsString + ', ' + ordersDB.UniQuery1.FieldByName('NAME').AsString);
    OrdersDB.UniQuery1.Next;
    count := count + 1;
  end;

  Memo2.Lines.Add('Showing ' + IntToStr(count) + ' entries');

end;

procedure TfQB.getCustomers();
var
  restClient: TRESTClient;
  restRequest: TRESTRequest;
  restResponse: TRESTResponse;
  param: TRESTRequestParameter;
  res: string;
  jsValue: TJSONValue;
  jsObj: TJSONObject;
  CustomerList: TJSONArray;
  pair: TJSONPair;
  ModifiedList: TJSONArray;
  ParsedCustomer, Customer: TJSONObject;
  I: integer;
  BillAddr: TJSONObject;
  ShipAddr: 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 + '/preferences';
  res := '/v3/company/' + companyID + '/query?query=select * from Customer&minorversion=75';  // '/customer/58';
  restRequest.Resource := res;
  ///query?query=select * from Customer where CompanyName = ' + quotedStr('TYOGA CONTAINER') + '&minorversion=75';


  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(res);
  Memo2.Lines.Add( jsObj.Format(2) );
  //CustomerList := TJSONArray(restResponse.JSONValue);

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

  LoadJSONArray( CustomerList );

  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
  inherited;
  ordersDB := TApiDatabase.Create(nil);
  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;

procedure TfQB.FormDestroy(Sender: TObject);
begin
  ordersDB.Free;
  inherited;
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.
