Skip to content

feat(quickbooks): Add missing invoice, estimate, and purchase order actions #16908

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

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

krikera
Copy link

@krikera krikera commented May 31, 2025

This PR addresses GitHub issue #16906 by implementing missing QuickBooks Online API endpoints that are available in Zapier but were missing from Pipedream. This brings the Pipedream QuickBooks integration to feature parity with Zapier for invoice, estimate, and purchase order operations.

🔧 New API Methods Added to quickbooks.app.mjs:

  • sendInvoice() - Send invoice via email
  • updateInvoice() - Update existing invoice
  • voidInvoice() - Void an invoice
  • createPurchaseOrder() - Create purchase orders
  • createEstimate() - Create estimates/quotes
  • sendEstimate() - Send estimate via email
  • updateEstimate() - Update existing estimate
  • getEstimate() - Retrieve estimate by ID

📋 New Prop Definitions:

  • estimateId - For selecting estimates with customer context
  • vendor - For selecting vendors for purchase orders

Fixes #16906

Summary by CodeRabbit

  • New Features
    • Added actions to create, update, send, and void invoices in QuickBooks.
    • Introduced actions to create, update, and send estimates by email.
    • Enabled creation of purchase orders with configurable line items.
    • Enhanced selection options for estimates and vendors in the user interface.
    • Added utilities to construct line items for sales and purchase transactions.

Copy link

vercel bot commented May 31, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

1 Skipped Deployment
Name Status Preview Comments Updated (UTC)
pipedream-docs-redirect-do-not-edit ⬜️ Ignored (Inspect) Jun 2, 2025 4:29pm

Copy link

vercel bot commented May 31, 2025

@krikera is attempting to deploy a commit to the Pipedreamers Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
Contributor

coderabbitai bot commented May 31, 2025

Walkthrough

This update introduces new QuickBooks action modules to create, send, update, and void invoices and estimates, as well as create purchase orders. It also adds supporting methods and property definitions to the QuickBooks app module, enabling these endpoints and their required input handling.

Changes

File(s) Change Summary
components/quickbooks/actions/create-estimate/create-estimate.mjs New action: Create Estimate with flexible line item input, validation, and API call.
components/quickbooks/actions/create-purchase-order/create-purchase-order.mjs New action: Create Purchase Order with dynamic line item support and API integration.
components/quickbooks/actions/send-estimate/send-estimate.mjs New action: Send Estimate by email via QuickBooks API, supports optional recipient email.
components/quickbooks/actions/send-invoice/send-invoice.mjs New action: Send Invoice by email via QuickBooks API, supports optional recipient email.
components/quickbooks/actions/update-estimate/update-estimate.mjs New action: Update Estimate with dynamic properties, line item validation, and concurrency control.
components/quickbooks/actions/update-invoice/update-invoice.mjs New action: Update Invoice with dynamic line items, field support, and concurrency control.
components/quickbooks/actions/void-invoice/void-invoice.mjs New action: Void Invoice with SyncToken handling and API call.
components/quickbooks/quickbooks.app.mjs Added propDefinitions for estimateId and vendor; added methods for invoice, purchase order, estimate operations.
components/quickbooks/common/utils.mjs Added helper functions to build sales and purchase line items from input context.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant ActionModule
    participant QuickBooksApp
    participant QuickBooksAPI

    User->>ActionModule: Provide input (e.g., create/update/send/void)
    ActionModule->>QuickBooksApp: Call appropriate method with input
    QuickBooksApp->>QuickBooksAPI: Make HTTP request to API endpoint
    QuickBooksAPI-->>QuickBooksApp: Return API response
    QuickBooksApp-->>ActionModule: Return processed result
    ActionModule-->>User: Output summary/result
Loading

Assessment against linked issues

Objective Addressed Explanation
Implement Create Invoice endpoint (POST /invoice) (#16906) No create invoice action/module or method observed in this PR.
Implement Send Invoice endpoint (POST /invoice/{invoiceId}/send) (#16906)
Implement Update Invoice endpoint (POST /invoice?operation=update) (#16906)
Implement Void Invoice endpoint (POST /invoice?operation=void) (#16906)
Implement Create Purchase Order endpoint (POST /purchaseorder) (#16906)
Implement Create Estimate endpoint (POST /estimate) (#16906)
Implement Send Estimate endpoint (POST /estimate/{estimateId}/send) (#16906)
Implement Update Estimate endpoint (POST /estimate?operation=update) (#16906)

Poem

In the fields of QuickBooks, where data flows free,
Rabbits code actions for invoice and P.O. spree!
Estimates are crafted, sent with a hop,
Updates and voids—no reason to stop.
With whiskers a-twitch and a carrot in hand,
This bunny brings endpoints just as you planned!
🐇✨

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

components/quickbooks/actions/create-estimate/create-estimate.mjs

Oops! Something went wrong! :(

ESLint: 8.57.1

Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'jsonc-eslint-parser' imported from /eslint.config.mjs
at Object.getPackageJSONURL (node:internal/modules/package_json_reader:255:9)
at packageResolve (node:internal/modules/esm/resolve:767:81)
at moduleResolve (node:internal/modules/esm/resolve:853:18)
at defaultResolve (node:internal/modules/esm/resolve:983:11)
at ModuleLoader.defaultResolve (node:internal/modules/esm/loader:799:12)
at #cachedDefaultResolve (node:internal/modules/esm/loader:723:25)
at ModuleLoader.resolve (node:internal/modules/esm/loader:706:38)
at ModuleLoader.getModuleJobForImport (node:internal/modules/esm/loader:307:38)
at #link (node:internal/modules/esm/module_job:170:49)

components/quickbooks/actions/create-purchase-order/create-purchase-order.mjs

Oops! Something went wrong! :(

ESLint: 8.57.1

Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'jsonc-eslint-parser' imported from /eslint.config.mjs
at Object.getPackageJSONURL (node:internal/modules/package_json_reader:255:9)
at packageResolve (node:internal/modules/esm/resolve:767:81)
at moduleResolve (node:internal/modules/esm/resolve:853:18)
at defaultResolve (node:internal/modules/esm/resolve:983:11)
at ModuleLoader.defaultResolve (node:internal/modules/esm/loader:799:12)
at #cachedDefaultResolve (node:internal/modules/esm/loader:723:25)
at ModuleLoader.resolve (node:internal/modules/esm/loader:706:38)
at ModuleLoader.getModuleJobForImport (node:internal/modules/esm/loader:307:38)
at #link (node:internal/modules/esm/module_job:170:49)

components/quickbooks/actions/update-invoice/update-invoice.mjs

Oops! Something went wrong! :(

ESLint: 8.57.1

Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'jsonc-eslint-parser' imported from /eslint.config.mjs
at Object.getPackageJSONURL (node:internal/modules/package_json_reader:255:9)
at packageResolve (node:internal/modules/esm/resolve:767:81)
at moduleResolve (node:internal/modules/esm/resolve:853:18)
at defaultResolve (node:internal/modules/esm/resolve:983:11)
at ModuleLoader.defaultResolve (node:internal/modules/esm/loader:799:12)
at #cachedDefaultResolve (node:internal/modules/esm/loader:723:25)
at ModuleLoader.resolve (node:internal/modules/esm/loader:706:38)
at ModuleLoader.getModuleJobForImport (node:internal/modules/esm/loader:307:38)
at #link (node:internal/modules/esm/module_job:170:49)

  • 2 others
✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@adolfo-pd adolfo-pd added the User submitted Submitted by a user label May 31, 2025
@pipedream-component-development
Copy link
Collaborator

Thank you so much for submitting this! We've added it to our backlog to review, and our team has been notified.

@pipedream-component-development
Copy link
Collaborator

Thanks for submitting this PR! When we review PRs, we follow the Pipedream component guidelines. If you're not familiar, here's a quick checklist:

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

♻️ Duplicate comments (1)
components/quickbooks/actions/update-invoice/update-invoice.mjs (1)

92-97: Verify if taxCodeId should be optional.

The taxCodeId prop is not marked as optional and is not being used in the update data, which seems inconsistent for an update operation.

🧹 Nitpick comments (2)
components/quickbooks/actions/create-purchase-order/create-purchase-order.mjs (1)

118-131: Improve validation error handling.

The validation logic is good but could be more specific and handle edge cases better.

Consider these improvements:

  async run({ $ }) {
-   if ((!this.numLineItems && !this.lineItemsAsObjects) || !this.vendorRefValue) {
-     throw new ConfigurationError("Must provide lineItems and vendorRefValue parameters.");
+   if (!this.vendorRefValue) {
+     throw new ConfigurationError("Vendor is required to create a purchase order.");
+   }
+
+   if (!this.numLineItems && !this.lineItemsAsObjects) {
+     throw new ConfigurationError("At least one line item is required. Either specify the number of line items or provide line items as objects.");
    }

    const lines = this.lineItemsAsObjects
      ? parseLineItems(this.lineItems)
      : this.buildLineItems();

+   if (!lines || lines.length === 0) {
+     throw new ConfigurationError("No valid line items were provided.");
+   }

    lines.forEach((line) => {
      if (line.DetailType !== "ItemBasedExpenseLineDetail" && line.DetailType !== "AccountBasedExpenseLineDetail") {
        throw new ConfigurationError("Line Item DetailType must be `ItemBasedExpenseLineDetail` or `AccountBasedExpenseLineDetail`");
      }
    });
components/quickbooks/actions/update-invoice/update-invoice.mjs (1)

149-164: Consider extracting common line item building logic.

The buildLineItems method is duplicated across all three action files. Consider extracting this to a shared utility.

You could create a shared utility in components/quickbooks/common/utils.mjs:

export function buildSalesLineItems(numLineItems, context) {
  const lineItems = [];
  for (let i = 1; i <= numLineItems; i++) {
    lineItems.push({
      DetailType: "SalesItemLineDetail",
      Amount: context[`amount_${i}`],
      SalesItemLineDetail: {
        ItemRef: {
          value: context[`item_${i}`],
        },
      },
    });
  }
  return lineItems;
}

This would reduce code duplication and make maintenance easier across all QuickBooks actions.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 480ec38 and c969b5c.

📒 Files selected for processing (8)
  • components/quickbooks/actions/create-estimate/create-estimate.mjs (1 hunks)
  • components/quickbooks/actions/create-purchase-order/create-purchase-order.mjs (1 hunks)
  • components/quickbooks/actions/send-estimate/send-estimate.mjs (1 hunks)
  • components/quickbooks/actions/send-invoice/send-invoice.mjs (1 hunks)
  • components/quickbooks/actions/update-estimate/update-estimate.mjs (1 hunks)
  • components/quickbooks/actions/update-invoice/update-invoice.mjs (1 hunks)
  • components/quickbooks/actions/void-invoice/void-invoice.mjs (1 hunks)
  • components/quickbooks/quickbooks.app.mjs (2 hunks)
🔇 Additional comments (19)
components/quickbooks/actions/send-estimate/send-estimate.mjs (1)

1-43: LGTM! Well-structured action with proper conditional logic.

This action follows good patterns for Pipedream QuickBooks actions:

  • Uses propDefinition for consistent ID selection
  • Properly handles optional email parameter
  • Clean conditional data construction
  • Appropriate success messaging

The implementation correctly handles the optional email parameter by conditionally adding it to the data object only when provided.

components/quickbooks/actions/send-invoice/send-invoice.mjs (1)

1-43: LGTM! Consistent implementation with send-estimate action.

This action correctly mirrors the send-estimate implementation with appropriate changes for invoices:

  • Uses invoiceId propDefinition consistently
  • Maintains the same conditional email logic pattern
  • Calls the corresponding sendInvoice method
  • Provides appropriate success messaging

The consistency between similar actions is excellent and makes the codebase more maintainable.

components/quickbooks/actions/create-estimate/create-estimate.mjs (4)

162-166: Good validation of DetailType values!

The validation correctly checks for allowed DetailType values and provides a clear error message.


5-212: Well-structured action implementation!

The action follows good patterns with:

  • Clear prop definitions and descriptions
  • Dynamic property generation based on user input
  • Proper error handling with ConfigurationError
  • Informative success message

79-84:

✅ Verification successful

Verify if taxCodeId should be optional.

The taxCodeId prop is not marked as optional, but it's not clear from the code whether it's always added to the request data or if it should be conditional.

Run the following script to check how taxCodeId is used in the codebase:


🏁 Script executed:

#!/bin/bash
# Search for taxCodeId usage in QuickBooks actions
rg -A 5 -B 5 "taxCodeId" --glob "components/quickbooks/**/*.mjs"

Length of output: 8098


🌐 Web query:

Is the taxCodeId field required when creating an estimate in QuickBooks Online API?

💡 Result:

In the QuickBooks Online API, the taxCodeId field is not required when creating an estimate. However, if your QuickBooks Online company is set up to charge sales tax, it's advisable to include the taxCodeId to specify the taxability of the estimate. This field determines whether the estimate is taxable or non-taxable. By default, QuickBooks Online provides two general tax codes:

  • TAX (Taxable)
  • NON (Non-Taxable)

If you don't specify a taxCodeId, QuickBooks Online will assign the default non-taxable code to all sales. (developer.intuit.com)

To specify a tax code when creating an estimate, you can include the taxCodeId in your API request. For example:

{
  "Line": [
    {
      "Amount": 100.00,
      "DetailType": "SalesItemLineDetail",
      "SalesItemLineDetail": {
        "ItemRef": {
          "value": "8",
          "name": "Lighting"
        },
        "TaxCodeRef": {
          "value": "TAX"
        }
      }
    }
  ],
  "TxnTaxDetail": {
    "TxnTaxCodeRef": {
      "value": "3",
      "name": "Tucson"
    }
  },
  "CustomerRef": {
    "value": "1"
  }
}

In this example, the TaxCodeRef for the line item is set to TAX, indicating that the item is taxable. The TxnTaxCodeRef in the TxnTaxDetail specifies the overall tax code for the transaction. (developer.intuit.com)

If your company is not set up to charge sales tax, you can omit the taxCodeId field, and QuickBooks Online will treat the estimate as non-taxable by default.

Citations:


taxCodeId is already optional via propDefinition

The taxCodeId prop in create-estimate.mjs uses the shared quickbooks.taxCodeId propDefinition, which is defined with optional: true in the app component. The QuickBooks Online API also treats taxCodeId as optional when creating an estimate. No changes needed.


154-156: ⚠️ Potential issue

Fix incorrect validation logic for line items.

The current validation logic incorrectly requires both numLineItems and lineItemsAsObjects to be falsy to throw an error. This would fail valid scenarios where the user provides lineItemsAsObjects but not numLineItems.

Apply this diff to fix the validation:

-    if ((!this.numLineItems && !this.lineItemsAsObjects) || !this.customerRefValue) {
+    if ((!this.lineItemsAsObjects && !this.numLineItems) || !this.customerRefValue) {
       throw new ConfigurationError("Must provide lineItems and customerRefValue parameters.");
     }

Additionally, consider splitting this into two separate checks for clearer error messages:

-    if ((!this.numLineItems && !this.lineItemsAsObjects) || !this.customerRefValue) {
-      throw new ConfigurationError("Must provide lineItems and customerRefValue parameters.");
-    }
+    if (!this.customerRefValue) {
+      throw new ConfigurationError("customerRefValue is required.");
+    }
+    if (!this.lineItemsAsObjects && !this.numLineItems) {
+      throw new ConfigurationError("Must provide either lineItems or numLineItems.");
+    }

Likely an incorrect or invalid review comment.

components/quickbooks/actions/update-estimate/update-estimate.mjs (2)

86-91: Verify if taxCodeId should be optional.

The taxCodeId prop is not marked as optional, and it's not being used in the update data. This seems inconsistent.

Since this is an update operation and not all fields need to be provided, taxCodeId should likely be optional. Verify if it's used elsewhere or should be added to the update data when provided.


160-230: Excellent update implementation!

The update logic properly:

  • Fetches the current estimate to obtain the required SyncToken
  • Only includes fields that are explicitly provided
  • Maintains data integrity by not overwriting fields with undefined values
components/quickbooks/actions/update-invoice/update-invoice.mjs (1)

200-201: Excellent boolean value handling!

Using typeof checks for boolean props is the correct approach to distinguish between false (a valid value) and undefined (field not provided).

components/quickbooks/quickbooks.app.mjs (10)

119-135: LGTM! Property definition follows existing patterns.

The estimateId property definition is well-structured and consistent with the existing invoiceId property. The mapper correctly extracts the ID, document number, and customer reference for display.


136-152: LGTM! Vendor property definition is properly implemented.

The vendor property definition follows the same pattern as the existing customer property and correctly maps vendor data for selection UI.


556-564: LGTM! Invoice sending method is correctly implemented.

The sendInvoice method properly destructures the invoiceId parameter and constructs the correct API endpoint path for sending invoices via email.


565-574: LGTM! Invoice update method follows API conventions.

The updateInvoice method correctly uses the POST method with the operation: "update" parameter, which is consistent with QuickBooks API conventions for update operations.


575-584: LGTM! Invoice void method properly implemented.

The voidInvoice method correctly implements the void operation using POST with the operation: "void" parameter, following QuickBooks API patterns.


585-591: LGTM! Purchase order creation method is consistent.

The createPurchaseOrder method follows the same pattern as other entity creation methods in the codebase, using POST to the appropriate endpoint.


592-598: LGTM! Estimate creation method properly implemented.

The createEstimate method is consistent with other create methods and uses the correct endpoint for estimate creation.


599-607: LGTM! Estimate sending method mirrors invoice sending pattern.

The sendEstimate method correctly follows the same pattern as sendInvoice, properly destructuring the estimateId parameter and constructing the send endpoint path.


608-617: LGTM! Estimate update method follows established conventions.

The updateEstimate method correctly implements the update operation using the same pattern as updateInvoice with the operation: "update" parameter.


618-625: LGTM! Estimate retrieval method is properly implemented.

The getEstimate method follows the same pattern as other get methods (like getInvoice), correctly constructing the path with the estimate ID.

…k - Add shared utilities, ensure optional props, enhance error handling
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
components/quickbooks/actions/update-invoice/update-invoice.mjs (3)

183-187: Improve line item validation with detailed error messages.

The validation logic is good but could provide more helpful error messages for debugging.

Consider enhancing the error message to include which line item failed validation:

 lines.forEach((line, index) => {
   if (line.DetailType !== "SalesItemLineDetail" && line.DetailType !== "GroupLineDetail" && line.DetailType !== "DescriptionOnly") {
-    throw new ConfigurationError("Line Item DetailType must be `SalesItemLineDetail`, `GroupLineDetail`, or `DescriptionOnly`");
+    throw new ConfigurationError(`Line Item at index ${index + 1} has invalid DetailType '${line.DetailType}'. Must be 'SalesItemLineDetail', 'GroupLineDetail', or 'DescriptionOnly'`);
   }
 });

192-218: Consider consolidating conditional assignments.

The multiple conditional assignments could be more maintainable with a helper function or object mapping approach.

Consider refactoring for better maintainability:

+// Helper function to conditionally add properties
+const addIfDefined = (target, source, mappings) => {
+  Object.entries(mappings).forEach(([sourceKey, targetConfig]) => {
+    const value = source[sourceKey];
+    if (value !== undefined && value !== null) {
+      if (typeof targetConfig === 'string') {
+        target[targetConfig] = value;
+      } else if (typeof targetConfig === 'object') {
+        target[targetConfig.key] = targetConfig.transform ? targetConfig.transform(value) : value;
+      }
+    }
+  });
+};

-if (this.dueDate) data.DueDate = this.dueDate;
-if (typeof this.allowOnlineCreditCardPayment === "boolean") data.AllowOnlineCreditCardPayment = this.allowOnlineCreditCardPayment;
-if (typeof this.allowOnlineACHPayment === "boolean") data.AllowOnlineACHPayment = this.allowOnlineACHPayment;
-if (this.docNumber) data.DocNumber = this.docNumber;
-if (this.billAddr) data.BillAddr = this.billAddr;
-if (this.shipAddr) data.ShipAddr = this.shipAddr;
-if (this.trackingNum) data.TrackingNum = this.trackingNum;
-if (this.privateNote) data.PrivateNote = this.privateNote;
+
+// Add simple field mappings
+addIfDefined(data, this, {
+  dueDate: 'DueDate',
+  docNumber: 'DocNumber',
+  billAddr: 'BillAddr',
+  shipAddr: 'ShipAddr',
+  trackingNum: 'TrackingNum',
+  privateNote: 'PrivateNote',
+});
+
+// Handle boolean fields explicitly
+if (typeof this.allowOnlineCreditCardPayment === "boolean") {
+  data.AllowOnlineCreditCardPayment = this.allowOnlineCreditCardPayment;
+}
+if (typeof this.allowOnlineACHPayment === "boolean") {
+  data.AllowOnlineACHPayment = this.allowOnlineACHPayment;
+}

224-226: Improve response validation.

The response validation could be more robust to handle edge cases.

Add better response validation:

-if (response) {
-  $.export("summary", `Successfully updated invoice with ID ${response.Invoice.Id}`);
-}
+if (response?.Invoice?.Id) {
+  $.export("summary", `Successfully updated invoice with ID ${response.Invoice.Id}`);
+} else {
+  throw new ConfigurationError("Failed to update invoice: Invalid response from QuickBooks API");
+}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c969b5c and 1009642.

📒 Files selected for processing (6)
  • components/quickbooks/actions/create-estimate/create-estimate.mjs (1 hunks)
  • components/quickbooks/actions/create-purchase-order/create-purchase-order.mjs (1 hunks)
  • components/quickbooks/actions/update-estimate/update-estimate.mjs (1 hunks)
  • components/quickbooks/actions/update-invoice/update-invoice.mjs (1 hunks)
  • components/quickbooks/actions/void-invoice/void-invoice.mjs (1 hunks)
  • components/quickbooks/common/utils.mjs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • components/quickbooks/actions/create-purchase-order/create-purchase-order.mjs
  • components/quickbooks/actions/void-invoice/void-invoice.mjs
  • components/quickbooks/actions/update-estimate/update-estimate.mjs
  • components/quickbooks/actions/create-estimate/create-estimate.mjs
🔇 Additional comments (1)
components/quickbooks/actions/update-invoice/update-invoice.mjs (1)

53-59: Previous feedback has been addressed.

The currencyRefValue prop is now correctly marked as optional, addressing the previous review comment.

… - Add comprehensive validation to buildSalesLineItems and buildPurchaseLineItems - Refactor conditional logic for better readability - Add error handling in update-invoice after getInvoice call
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1009642 and 98347b0.

📒 Files selected for processing (5)
  • components/quickbooks/actions/create-estimate/create-estimate.mjs (1 hunks)
  • components/quickbooks/actions/create-purchase-order/create-purchase-order.mjs (1 hunks)
  • components/quickbooks/actions/update-estimate/update-estimate.mjs (1 hunks)
  • components/quickbooks/actions/update-invoice/update-invoice.mjs (1 hunks)
  • components/quickbooks/common/utils.mjs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • components/quickbooks/actions/create-estimate/create-estimate.mjs
  • components/quickbooks/actions/create-purchase-order/create-purchase-order.mjs
  • components/quickbooks/actions/update-invoice/update-invoice.mjs
🧰 Additional context used
🪛 Biome (1.9.4)
components/quickbooks/common/utils.mjs

[error] 62-62: Do not access Object.prototype method 'hasOwnProperty' from target object.

It's recommended using Object.hasOwn() instead of using Object.hasOwnProperty().
See MDN web docs for more details.

(lint/suspicious/noPrototypeBuiltins)


[error] 65-65: Do not access Object.prototype method 'hasOwnProperty' from target object.

It's recommended using Object.hasOwn() instead of using Object.hasOwnProperty().
See MDN web docs for more details.

(lint/suspicious/noPrototypeBuiltins)


[error] 117-117: Do not access Object.prototype method 'hasOwnProperty' from target object.

It's recommended using Object.hasOwn() instead of using Object.hasOwnProperty().
See MDN web docs for more details.

(lint/suspicious/noPrototypeBuiltins)


[error] 120-120: Do not access Object.prototype method 'hasOwnProperty' from target object.

It's recommended using Object.hasOwn() instead of using Object.hasOwnProperty().
See MDN web docs for more details.

(lint/suspicious/noPrototypeBuiltins)

🔇 Additional comments (4)
components/quickbooks/actions/update-estimate/update-estimate.mjs (4)

14-104: Well-structured props definition.

The props are well-defined with appropriate optional flags and good reuse of propDefinitions from the main QuickBooks app. The past review comment about currencyRefValue being optional has been properly addressed.


105-147: Excellent dynamic props implementation.

The conditional property loading pattern is well-implemented with clean logic flow and proper async options for item selection. The early return pattern and consistent naming for dynamic props makes this very readable.


148-165: Clean helper methods with good abstraction.

The buildLineItems delegation and addIfDefined helper provide clean abstractions that reduce code repetition. The mapping object pattern in addIfDefined is particularly elegant for conditional property assignment.


166-240: Solid implementation of estimate update logic.

The implementation properly handles QuickBooks concurrency control with SyncToken retrieval, has good validation of line item DetailType values, and includes proper error handling. The conditional building of the update payload and use of helper methods keeps the code clean and maintainable.

Comment on lines +103 to +180
export function buildPurchaseLineItems(numLineItems, context) {
// Validate numLineItems parameter
if (typeof numLineItems !== "number" || !Number.isInteger(numLineItems) || numLineItems <= 0) {
throw new ConfigurationError("numLineItems must be a positive integer");
}

// Validate context parameter
if (!context || typeof context !== "object" || Array.isArray(context)) {
throw new ConfigurationError("context must be an object");
}

// Validate required keys exist for each line item
const missingKeys = [];
for (let i = 1; i <= numLineItems; i++) {
if (!context.hasOwnProperty(`amount_${i}`)) {
missingKeys.push(`amount_${i}`);
}
if (!context.hasOwnProperty(`item_${i}`)) {
missingKeys.push(`item_${i}`);
}
}

if (missingKeys.length > 0) {
throw new ConfigurationError(`Missing required keys in context: ${missingKeys.join(", ")}`);
}

// Validate amount values are valid numbers
const invalidAmounts = [];
for (let i = 1; i <= numLineItems; i++) {
const amount = context[`amount_${i}`];
if (amount !== undefined && amount !== null && amount !== "" &&
(typeof amount !== "number" && (typeof amount !== "string" || isNaN(parseFloat(amount))))) {
invalidAmounts.push(`amount_${i}`);
}
}

if (invalidAmounts.length > 0) {
throw new ConfigurationError(`Invalid amount values for: ${invalidAmounts.join(", ")}. Amounts must be valid numbers.`);
}

// Validate detailType values if provided
const validDetailTypes = ["ItemBasedExpenseLineDetail", "AccountBasedExpenseLineDetail"];
const invalidDetailTypes = [];
for (let i = 1; i <= numLineItems; i++) {
const detailType = context[`detailType_${i}`];
if (detailType && !validDetailTypes.includes(detailType)) {
invalidDetailTypes.push(`detailType_${i}: ${detailType}`);
}
}

if (invalidDetailTypes.length > 0) {
throw new ConfigurationError(`Invalid detailType values for: ${invalidDetailTypes.join(", ")}. Valid types are: ${validDetailTypes.join(", ")}`);
}

const lineItems = [];
for (let i = 1; i <= numLineItems; i++) {
const detailType = context[`detailType_${i}`] || "ItemBasedExpenseLineDetail";

// Extract conditional logic into clear variables
const isItemBased = detailType === "ItemBasedExpenseLineDetail";
const detailPropertyName = isItemBased ? "ItemBasedExpenseLineDetail" : "AccountBasedExpenseLineDetail";
const refPropertyName = isItemBased ? "ItemRef" : "AccountRef";

// Build line item with clearer structure
const lineItem = {
DetailType: detailType,
Amount: context[`amount_${i}`],
[detailPropertyName]: {
[refPropertyName]: {
value: context[`item_${i}`],
},
},
};

lineItems.push(lineItem);
}
return lineItems;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Replace hasOwnProperty with Object.hasOwn() and excellent refactoring work!

The conditional logic refactoring with clear variable names is excellent - much more readable than nested conditionals. However, the same hasOwnProperty security issue needs addressing.

Apply this diff:

   for (let i = 1; i <= numLineItems; i++) {
-    if (!context.hasOwnProperty(`amount_${i}`)) {
+    if (!Object.hasOwn(context, `amount_${i}`)) {
       missingKeys.push(`amount_${i}`);
     }
-    if (!context.hasOwnProperty(`item_${i}`)) {
+    if (!Object.hasOwn(context, `item_${i}`)) {
       missingKeys.push(`item_${i}`);
     }
   }

The refactoring from lines 162-164 with isItemBased, detailPropertyName, and refPropertyName variables significantly improves code readability compared to inline conditionals.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function buildPurchaseLineItems(numLineItems, context) {
// Validate numLineItems parameter
if (typeof numLineItems !== "number" || !Number.isInteger(numLineItems) || numLineItems <= 0) {
throw new ConfigurationError("numLineItems must be a positive integer");
}
// Validate context parameter
if (!context || typeof context !== "object" || Array.isArray(context)) {
throw new ConfigurationError("context must be an object");
}
// Validate required keys exist for each line item
const missingKeys = [];
for (let i = 1; i <= numLineItems; i++) {
if (!context.hasOwnProperty(`amount_${i}`)) {
missingKeys.push(`amount_${i}`);
}
if (!context.hasOwnProperty(`item_${i}`)) {
missingKeys.push(`item_${i}`);
}
}
if (missingKeys.length > 0) {
throw new ConfigurationError(`Missing required keys in context: ${missingKeys.join(", ")}`);
}
// Validate amount values are valid numbers
const invalidAmounts = [];
for (let i = 1; i <= numLineItems; i++) {
const amount = context[`amount_${i}`];
if (amount !== undefined && amount !== null && amount !== "" &&
(typeof amount !== "number" && (typeof amount !== "string" || isNaN(parseFloat(amount))))) {
invalidAmounts.push(`amount_${i}`);
}
}
if (invalidAmounts.length > 0) {
throw new ConfigurationError(`Invalid amount values for: ${invalidAmounts.join(", ")}. Amounts must be valid numbers.`);
}
// Validate detailType values if provided
const validDetailTypes = ["ItemBasedExpenseLineDetail", "AccountBasedExpenseLineDetail"];
const invalidDetailTypes = [];
for (let i = 1; i <= numLineItems; i++) {
const detailType = context[`detailType_${i}`];
if (detailType && !validDetailTypes.includes(detailType)) {
invalidDetailTypes.push(`detailType_${i}: ${detailType}`);
}
}
if (invalidDetailTypes.length > 0) {
throw new ConfigurationError(`Invalid detailType values for: ${invalidDetailTypes.join(", ")}. Valid types are: ${validDetailTypes.join(", ")}`);
}
const lineItems = [];
for (let i = 1; i <= numLineItems; i++) {
const detailType = context[`detailType_${i}`] || "ItemBasedExpenseLineDetail";
// Extract conditional logic into clear variables
const isItemBased = detailType === "ItemBasedExpenseLineDetail";
const detailPropertyName = isItemBased ? "ItemBasedExpenseLineDetail" : "AccountBasedExpenseLineDetail";
const refPropertyName = isItemBased ? "ItemRef" : "AccountRef";
// Build line item with clearer structure
const lineItem = {
DetailType: detailType,
Amount: context[`amount_${i}`],
[detailPropertyName]: {
[refPropertyName]: {
value: context[`item_${i}`],
},
},
};
lineItems.push(lineItem);
}
return lineItems;
}
// Validate required keys exist for each line item
const missingKeys = [];
for (let i = 1; i <= numLineItems; i++) {
if (!Object.hasOwn(context, `amount_${i}`)) {
missingKeys.push(`amount_${i}`);
}
if (!Object.hasOwn(context, `item_${i}`)) {
missingKeys.push(`item_${i}`);
}
}
🧰 Tools
🪛 Biome (1.9.4)

[error] 117-117: Do not access Object.prototype method 'hasOwnProperty' from target object.

It's recommended using Object.hasOwn() instead of using Object.hasOwnProperty().
See MDN web docs for more details.

(lint/suspicious/noPrototypeBuiltins)


[error] 120-120: Do not access Object.prototype method 'hasOwnProperty' from target object.

It's recommended using Object.hasOwn() instead of using Object.hasOwnProperty().
See MDN web docs for more details.

(lint/suspicious/noPrototypeBuiltins)

🤖 Prompt for AI Agents
In components/quickbooks/common/utils.mjs between lines 103 and 180, replace all
uses of hasOwnProperty with Object.has() for safer property existence checks.
Specifically, update the validation loops checking for required keys in the
context object to use Object.has(context, key) instead of
context.hasOwnProperty(key). Keep the existing refactoring with clear variable
names for conditional logic as it improves readability.

Copy link
Collaborator

@GTFalcao GTFalcao left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @krikera , thanks for your valuable contribution!

I left one comment regarding a description and reusability improvement. Other than that, this looks good to me.

props.lineItems = {
type: "string[]",
label: "Line Items",
description: "Line items of an estimate. Set DetailType to `SalesItemLineDetail`, `GroupLineDetail`, or `DescriptionOnly`. Example: `{ \"DetailType\": \"SalesItemLineDetail\", \"Amount\": 100.0, \"SalesItemLineDetail\": { \"ItemRef\": { \"name\": \"Services\", \"value\": \"1\" } } }`",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should link to the relevant documentation here if possible (as well as on the other instances of this prop in other actions - ideally we would import this object from a common file)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
User submitted Submitted by a user
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[ACTION] Add Quickbook Endpoints (Missing but are available on Zapier!)
4 participants