apigrate / quickbooks

Connection library for Intuit QuickBooks
Apache License 2.0
11 stars 4 forks source link

@apigrate/quickbooks

A transparent one-stop library for interacting with the QuickBooks Online API. It supports all the features you need to interact with the QuickBooks Online Accounting API, including:

  1. automatic OAuth2 token refresh (including an event handler)
  2. support for native promises
  3. unopinionated error handling
  4. convenience method for constructor OAuth URLs
  5. and most importantly, complete coverage of the QuickBooks Online Accounting API.

Version 4.x Changes

Now uses the Intuit discovery documents, dynamically loading the correct URLs from Intuit OAuth.

Changes:

Breaking Changes:

Supported Entities

Supported Transactional Entities: Entity query create get by id update delete void
Bill
BillPayment
CreditMemo
Deposit
Estimate
Invoice
JournalEntry
Payment
Purchase
Purchaseorder
RefundReceipt
SalesReceipt
TimeActivity
Transfer
VendorCredit
Named List Entities: Entity reference query create get by id update delete
Account account  
Budget budget      
Class class  
CompanyCurrency companycurrency  
Customer customer  
Department department  
Employee employee  
Item item  
Journalcode journalcode  
PaymentMethod paymentmethod  
TaxAgency taxagency    
TaxCode taxcode    
TaxRate taxrate    
TaxService taxservice/taxcode      
Term term  
Vendor vendor  
Supporting Entities: Entity reference query create get by id update delete
Attachable attachable
CompanyInfo companyinfo    
Preferences preferences    
Reports: Report
AccountListDetailReport
APAgingDetailReport
APAgingSummaryReport
ARAgingDetailReport
ARAgingSummaryReport
BalanceSheetReport
CashFlowReport
CustomerBalanceReport
CustomerBalanceDetailReport
CustomerIncomeReport
GeneralLedgerReport
GeneralLedgerReportFR
InventoryValuationSummaryReport
JournalReport
ProfitAndLossReport
ProfitAndLossDetailReport
SalesByClassSummaryReport
SalesByCustomerReport
SalesByDepartmentReport
SalesByProductReport
TaxSummaryReport
TransactionListReport
TrialBalanceReportFR
TrialBalanceReport
VendorBalanceReport
VendorBalanceDetailReport
VendorExpensesReport

Usage

let {QboConnector} = require('@apigrate/quickbooks');

let connector = new QboConnector(
  client_id,        
  client_secret,    
  redirect_uri,
  access_token,     
  refresh_token,    
  realm_id,         
  minorversion
);

//Get the accounting API object.
let qbo = await connector.accountingApi();  

//Now you can make API calls!

Constructor options:

The connector will automatically use any provided credentials to renew the access_token when it expires. The connector emits a token.refreshed event internally when it does this. Implement your own listener function to store credentials.


//Event hook to handle a token refresh.
connector.on('token.refreshed', function(credentials){
  console.log('Token was refreshed.');

  //Use this function to store/update your OAuth data

  //The credentials object may have the following properties:
  credentials.access_token; //Bearer token for API calls.
  credentials.expires_in; //how long before access token expires, in s
  credentials.refresh_token; //for getting another access token
  credentials.x_refresh_token_expires_in; //how long before refresh_token expires, in s
  credentials.realm_id; //the qbo company id

});

Making API Calls

You make QuickBooks Accounting API calls by using the object returned from the connector.accountingApi() method (referred hereafter in examples as qbo). This entity wraps the QuickBooks Online Accounting API.

Query

Query any kind of object using the Intuit query syntax.

Entity.query(statement, opts)

let result = await qbo.Item.query(
  `select * from Item where Active=true and Type='Inventory'`
  );

The result is:

{
  "QueryResponse": {
    "Item": [
      {
        "Name": "Pump",
        "Description": "Fountain Pump",
        "Active": true,
        "FullyQualifiedName": "Pump",
        "Taxable": true,
        "UnitPrice": 15,
        "Type": "Inventory",
        "IncomeAccountRef": {
          "value": "79",
          "name": "Sales of Product Income"
        },
        "PurchaseDesc": "Fountain Pump",
        "PurchaseCost": 10,
        "ExpenseAccountRef": {
          "value": "80",
          "name": "Cost of Goods Sold"
        },
        "AssetAccountRef": {
          "value": "81",
          "name": "Inventory Asset"
        },
        "TrackQtyOnHand": true,
        "QtyOnHand": 25,
        "InvStartDate": "2018-03-29",
        "domain": "QBO",
        "sparse": false,
        "Id": "11",
        "SyncToken": "3",
        "MetaData": {
          "CreateTime": "2018-03-26T10:46:45-07:00",
          "LastUpdatedTime": "2018-03-29T13:16:17-07:00"
        }
      }
    ]
  }
}

Note that while the Intuit query API endpoint is generic across all entities, the connector will provide some additional safeguard validation if you use the query method on the appropriate entity endpoint. For example, we recommend using qbo.Items.query instead of qbo.query for clarity in your code.

Get By ID

All you need is an entity ID and you can retrieve the full entity details.

Entity.get(id, opts)

Example: get an Item:

let result = await qbo.Item.get(11);

The result is:

{
  "Item": {
    "Name": "Widget",
    "Active": true,
    "FullyQualifiedName": "Widget",
    "Taxable": false,
    "UnitPrice": 0,
    "Type": "Service",
    "IncomeAccountRef": {
      "value": "79",
      "name": "Sales of Product Income"
    },
    "PurchaseCost": 0,
    "TrackQtyOnHand": false,
    "domain": "QBO",
    "sparse": false,
    "Id": "21",
    "SyncToken": "0",
    "MetaData": {
      "CreateTime": "2018-11-13T14:41:34-08:00",
      "LastUpdatedTime": "2018-11-13T14:41:34-08:00"
    }
  },
  "time": "2018-11-13T15:08:40.458-08:00"
}

Create

The .create() method is used to create entities. Keep in mind, some fields can be conditionally required depending on the type of entity you are creating.

Entity.create(payload, opts)

Example: Creating an Item

let result = await qbo.Item.create({
  "Name": 'Widget',
  "Type": 'Inventory',
  "IncomeAccountRef": {
      "value": "79",
      "name": "Sales of Product Income"
  }
});

The result is:

{
  "Item": {
    "Name": "Widget",
    "Active": true,
    "FullyQualifiedName": "Widget",
    "Taxable": false,
    "UnitPrice": 0,
    "Type": "Service",
    "IncomeAccountRef": {
      "value": "79",
      "name": "Sales of Product Income"
    },
    "PurchaseCost": 0,
    "TrackQtyOnHand": false,
    "domain": "QBO",
    "sparse": false,
    "Id": "21",
    "SyncToken": "0",
    "MetaData": {
      "CreateTime": "2018-11-13T14:41:34-08:00",
      "LastUpdatedTime": "2018-11-13T14:41:34-08:00"
    }
  },
  "time": "2018-11-13T14:41:34.885-08:00"
}

Example: Create a Bill

let result = await qbo.Bill.create({
  "Line": [
    {
      "DetailType": "AccountBasedExpenseLineDetail", 
      "Amount": 200.0, 
      "Id": "1", 
      "AccountBasedExpenseLineDetail": {
        "AccountRef": {
          "value": "7"
        }
      }
    }
  ], 
  "VendorRef": {
    "value": "42"
  }
});

The result is:

{
  "Bill": {
    "DueDate": "2020-08-25",
    "Balance": 200,
    "domain": "QBO",
    "sparse": false,
    "Id": "145",
    "SyncToken": "0",
    "MetaData": {
      "CreateTime": "2020-08-25T06:27:05-07:00",
      "LastUpdatedTime": "2020-08-25T06:27:05-07:00"
    },
    "TxnDate": "2020-08-25",
    "CurrencyRef": {
      "value": "USD",
      "name": "United States Dollar"
    },
    "Line": [
      {
        "Id": "1",
        "LineNum": 1,
        "Amount": 200,
        "LinkedTxn": [],
        "DetailType": "AccountBasedExpenseLineDetail",
        "AccountBasedExpenseLineDetail": {
          "AccountRef": {
            "value": "7",
            "name": "Advertising"
          },
          "BillableStatus": "NotBillable",
          "TaxCodeRef": {
            "value": "NON"
          }
        }
      }
    ],
    "VendorRef": {
      "value": "42",
      "name": "Lee Advertising"
    },
    "APAccountRef": {
      "value": "33",
      "name": "Accounts Payable (A/P)"
    },
    "TotalAmt": 200
  },
  "time": "2020-08-25T06:27:05.412-07:00"
}

Update

The and .update() method is used to update an API entity. When updating entities, most entities support a "full update" mode, where you are expected to send the entire set of fields to be updated (anything missing is set to to null). Therefore, it is recommended you:

  1. fetch the full entity first,
  2. modify the fields you want to changes,
  3. send the full object back as part of the update operation.
    See the Intuit QuickBooks Online API Documentation to find out more for further details on the entity you are working with.

Entity.update(payload, opts)

Example: Update an Item

let existing = await qbo.Item.get(19);
existing.Item.Name="Rubber Ducky";
let result = await qbo.Item.update(existing.Item);

The result is:

{
  "Item": {
    "Name": "Rubber Ducky",
    "Active": true,
    "FullyQualifiedName": "Rubber Ducky",
    "Taxable": false,
    "UnitPrice": 0,
    "Type": "Service",
    "IncomeAccountRef": {
      "value": "79",
      "name": "Sales of Product Income"
    },
    "PurchaseCost": 0,
    "TrackQtyOnHand": false,
    "domain": "QBO",
    "sparse": false,
    "Id": "21",
    "SyncToken": "1",
    "MetaData": {
      "CreateTime": "2018-11-13T14:41:34-08:00",
      "LastUpdatedTime": "2019-08-28T12:05:02-08:00"
    }
  },
  "time": "2019-08-28T12:05:02.148-08:00"
}

Delete

Most transactional entities support deletion, but only a few named-list entities do. When deleting, you'll need both the Id and the SyncToken. Similar to update() method, it is usually best to retrieve an entity immediately before you delete it to obtain the latest SyncToken.

You can think of SyncToken as a "version number" of the entity you're working with. It is a mechanism to prevent two people from simultaneously making changes to the same entity at the same time.

Entity.delete(payload, opts)

Example: Delete a Bill

let result = await qbo.Bill.delete({
  "Id": 145,
  "SyncToken": 0 
});

The result is:

{
  "Bill": {
    "domain": "QBO",
    "status": "Deleted",
    "Id": "145"
  },
  "time": "2020-08-25T06:48:57.291-07:00"
}

For entites not supporting a "hard delete", usually there's a way to "soft-delete" them by setting Active=false using the update() method, or something similar.

Voids

Voids are supported for certain Intuit entities (BillPayment, Invoice, Payment, SalesReceipt). These are similar to updates, but you must use .voidTransaction() method to perform voids.

Entity.voidTransaction(payload, opts)

Batch Requests

Batch requests (submitting multiple operations with one request) ARE supported! Here is a code example. This deletes multiple time activities, but you can mix your own types of transactions. They do not need to be the same type of entity or operation.

Entity.batch(payload, opts)

try{
  let activities = [
    {Id: 123489, SyncToken: 0},
    {Id: 178275, SyncToken: 0},
    {Id: 189085, SyncToken: 0},
    ///...
    {Id: 190239, SyncToken: 0},
  ];//Only Id and SyncToken are needed on each for delete

  let batch = {
    BatchItemRequest: []
  }

  for(let i=0; i<activities.length; i++){
    let activityToDelete = activities[i];

    batch.BatchItemRequest.push({
      bId: i,
      operation: "delete",
      TimeActivity: {
        Id: activityToDelete.Id,
        SyncToken: activityToDelete.SyncToken
      }
    });
  }

  //Invoke Batch API
  await qbo.batch(batch);

} catch (err) {
  //...
}

Note that since you can mix the type of entity on batch requests, this method is not namespaced on entities (it is available directly on the qbo object).

Run a Report

Run any report simply by using the query method on each Report entity, providing report input parameters on the argument as a hash.

ReportEntity.query(parms, opts)

Example: Run the Customer Income Report

let result = await qbo.CustomerIncomeReport.query(
  {
    start_date: '2019-01-01', 
    end_date: '2020-04-01'
  }
);

The result is:

{
  "Header": {
    "Time": "2020-08-21T15:04:49-07:00",
    "ReportName": "CustomerIncome",
    "ReportBasis": "Accrual",
    "StartPeriod": "2019-01-01",
    "EndPeriod": "2020-04-01",
    "Currency": "USD",
    "Option": [
      {
        "Name": "NoReportData",
        "Value": "false"
      }
    ]
  },
  "Columns": {
    ...etc
  },
  "Rows": {
    ...etc
  }
}

(Some detail removed for length)

Error Handling

API-specific errors (typically HTTP-4xx) responses, are trapped and thrown with the ApiError class. An ApiThrottlingError is also available. It is a subclass of ApiError and is thrown when HTTP-429 responses are encountered, indicating the API request limits were reached.

Example: Attempted create that fails because of missing fields

const {ApiError} = require('@apigrate/quickbooks');
try{
  let result = qbo.Item.create({
    "Name": "Body Armor",
    "Type": "Inventory"
    //This will fail because there are other fields required...
  });

} catch (err){
  if(err instanceof ApiError){
    err.message; //contains a readable, parsed error message,
    err.payload; //the object payload of the error, if one was returned
  }

}

err.payload example:

{
  "Fault": {
    "Error": [
      {
        "Message": "Object Not Found",
        "Detail": "Object Not Found : Something you're trying to use has been made inactive. Check the fields with accounts, customers, items, vendors or employees.",
        "code": "610",
        "element": ""
      }
    ],
    "type": "ValidationFault"
  },
  "time": "2018-11-13T15:21:29.433-08:00"
}

Note that Fault.Error is an array, although most of the time there is just one error. For each error, the Message is the high-level explanation of the error.

OAuth getIntuitAuthorizationUrl = function(state)

The connector also provides a instance method for constructing a client-to-Intuit URL that initiates the user OAuth2 authorization process.

Parameters:

  1. state (string) Provides any state that might be useful to your application upon receipt of the response. The Intuit Authorization Server roundtrips this parameter, so your application receives the same value it sent. Including a CSRF token in the state is recommended.

Returns an authorization URL string with all parameters set and encoded. You use this to make your own call, typically on a UI component. Note, when the redirect_uri is invoked after the user has authenticated, it will contain the following query parameters:

  1. code (this is what you exchange for an access token and refresh token)
  2. realmId - this identifies the QBO company. Per Intuit's instructions it should be stored securely.