diff --git a/discover/README.md b/discover/README.md index 5c707c3..05ec81e 100644 --- a/discover/README.md +++ b/discover/README.md @@ -18,19 +18,21 @@ cd Prompt-Library/discover # 2. Configure your settings # Edit prompt-config.json (see Configuration section below) -# 3. Generate a prompt (interactive — choose 1 for Dimensions, 2 for Projects) +# 3. Generate a prompt (interactive — choose 1–4) node merge-prompt.js # Or use a custom config profile node merge-prompt.js --config config-java-bill.json ``` -The script prompts you to pick which template to generate. Run it twice to produce both: +The script prompts you to pick which template to generate. Run it once per template you need: | Choice | Generated File | Use Case | |---|---|---| | `1` | `generated-prompts/dimensions-ready-prompt.md` | Dimensions API — tag transactions with custom dimensions | | `2` | `generated-prompts/projects-ready-prompt.md` | Projects API — create projects and project estimates | +| `3` | `generated-prompts/custom-fields-ready-prompt.md` | Custom Fields API — discover, attach, and read custom field values | +| `4` | `generated-prompts/sales-tax-ready-prompt.md` | Sales Tax API — calculate sale transaction tax | Copy the contents of a generated prompt into your preferred AI coding assistant (e.g., Copilot, Cursor, ChatGPT, Windsurf) to generate a complete, runnable integration project. @@ -43,12 +45,18 @@ Prompt-Library/discover/ ├── merge-prompt.js # Prompt generator script ├── instructions.md # Reference — supported values & examples ├── dimensions/ -│ └── prompt-template-dimensions.md # Template — Dimensions API workflow +│ └── prompt-template-dimensions.md # Template — Dimensions API workflow ├── projects/ -│ └── prompt-template-projects.md # Template — Projects & Estimates workflow +│ └── prompt-template-projects.md # Template — Projects & Estimates workflow +├── custom-fields/ +│ └── prompt-template-custom-fields.md # Template — Custom Fields API workflow +├── sales-tax/ +│ └── prompt-template-sales-tax.md # Template — Sales Tax calculation workflow ├── generated-prompts/ -│ ├── dimensions-ready-prompt.md # Generated — Dimensions API prompt -│ └── projects-ready-prompt.md # Generated — Projects API prompt +│ ├── dimensions-ready-prompt.md # Generated — Dimensions API prompt +│ ├── projects-ready-prompt.md # Generated — Projects API prompt +│ ├── custom-fields-ready-prompt.md # Generated — Custom Fields API prompt +│ └── sales-tax-ready-prompt.md # Generated — Sales Tax API prompt └── README.md # This file ``` @@ -119,26 +127,23 @@ Refer to `instructions.md` for more detailed examples, including Purchase Order ``` ┌─────────────────────┐ ┌──────────────────────┐ -│ prompt-config.json │────▶│ merge-prompt.js │ -│ (your settings) │ │ (asks: 1 or 2?) │ +│ prompt-config.json │────▶│ merge-prompt.js │ +│ (your settings) │ │ (asks: 1–4?) │ └─────────────────────┘ └──────────┬───────────┘ │ - ┌───────────────┴───────────────┐ - choice 1 choice 2 - ▼ ▼ -┌─────────────────────────────────────┐ ┌─────────────────────────────────┐ -│ dimensions/prompt-template- │ │ projects/prompt-template- │ -│ dimensions.md (Dimensions template) │ │ projects.md (Projects template) │ -└────────────┬────────────────────────┘ └────────────┬────────────────────┘ - ▼ ▼ -┌──────────────────────────────────┐ ┌──────────────────────────────────┐ -│ generated-prompts/ │ │ generated-prompts/ │ -│ dimensions-ready-prompt.md │ │ projects-ready-prompt.md │ -│ (generated, AI-ready) │ │ (generated, AI-ready) │ -└──────────────────────────────────┘ └──────────────────────────────────┘ + ┌───────────────┬───────────────────┼───────────────────┬───────────────┐ + choice 1 choice 2 choice 3 choice 4 + ▼ ▼ ▼ ▼ + dimensions/ projects/ custom-fields/ sales-tax/ + prompt- prompt- prompt- prompt- + template- template- template- template- + dimensions.md projects.md custom-fields.md sales-tax.md + │ │ │ │ + ▼ ▼ ▼ ▼ + generated-prompts/…-ready-prompt.md (generated, AI-ready) ``` -`merge-prompt.js` reads the selected template, replaces every `{{placeholder}}` with the corresponding value from `prompt-config.json`, and writes the result as a ready-to-use prompt file. Run the script once per template you want to generate. +`merge-prompt.js` reads the selected template, replaces every `{{placeholder}}` with the corresponding value from `prompt-config.json`, and writes the result as a ready-to-use prompt file to `generated-prompts/`. Run the script once per template you want to generate. ### Safeguards @@ -192,6 +197,19 @@ Generates code to discover IES Dimensions (custom categorization) via GraphQL, t ### Projects & Estimates (`projects/prompt-template-projects.md`) Generates code to verify project eligibility, discover or create projects via GraphQL, create Project Estimates via REST, and display the results with markup calculations. +### Custom Fields (`custom-fields/prompt-template-custom-fields.md`) +Generates code to discover active custom field definitions via GraphQL, attach custom metadata to QuickBooks transactions (and `Customer`/`Vendor` entities) via REST V3, and read them back with human-readable labels. + +- **Silver+ partner tier required** — the Custom Fields API is a premium API; Builder-tier apps receive `403 Forbidden`. +- **Production-only GraphQL** — the Custom Fields GraphQL API has no documented sandbox endpoint; test with free non-expiring partner test accounts. (The REST V3 read/write calls still support sandbox.) +- **`legacyIDV2` bridge** — the GraphQL definition returns both `id` (opaque) and `legacyIDV2` (numeric); REST V3 payloads must set `DefinitionId = legacyIDV2`, not `id`. + +### Sales Tax (`sales-tax/prompt-template-sales-tax.md`) +Generates code to calculate sale transaction tax via the GraphQL `indirectTaxCalculateSaleTransactionTax` mutation — computing per-line and aggregate tax from ship-from/ship-to addresses, line items, and shipping fees. + +- **Scope required** — `indirect-tax.tax-calculation.quickbooks`. +- **Calculation-only** — the API returns a tax calculation; it does not itself create or post a transaction. + ## Troubleshooting | Problem | Solution | diff --git a/discover/custom-fields/README.md b/discover/custom-fields/README.md deleted file mode 100644 index 983f133..0000000 --- a/discover/custom-fields/README.md +++ /dev/null @@ -1,293 +0,0 @@ -# Custom Fields Prompt - -Generates AI-ready prompts for integrating with the **QuickBooks Custom Fields APIs** — discovers active custom field definitions via **GraphQL**, attaches custom metadata to QuickBooks transactions and to `Customer`/`Vendor` entities via **REST V3**, and reads them back with human-readable labels. - -> ℹ️ **Production only.** The Custom Fields GraphQL API does **not** have a documented sandbox endpoint — the official docs list only the production GraphQL host. Test using free non-expiring partner test accounts (request via the Intuit Partner Program). The REST V3 calls in Tasks 2 & 3 do still support sandbox for the entity payload, but the Task 1 / 4 / 5 GraphQL operations are production-only. -> -> ⚠️ **Silver+ partner tier required.** The Custom Fields API is a premium API restricted to Silver, Gold, and Platinum partners — Builder-tier apps will receive `403 Forbidden`. Upgrade on the [Intuit Developer Portal](https://developer.intuit.com); if access is missing on a paid tier, open a support ticket. - -## What This Prompt Produces - -When you paste the generated prompt into an AI coding assistant (Claude, Copilot, Cursor, ChatGPT, etc.), it generates a complete integration covering: - -1. **Task 1 — Discover Custom Field Definitions (GraphQL)** - - POSTs the `appFoundationsCustomFieldDefinitions` query to the GraphQL endpoint, with a `filters` input (plural — `AppFoundations_CustomExtensionsDefinitionFilterBy`) using primitive fields (`active: true`, `subType: ""`) — no `{ equals: ... }` wrappers - - Extracts `id` (lowercase — schema does NOT expose `Id`), **`legacyIDV2`**, `label`, `dataType`, `required`, `dropDownOptions[]`, and `associations[]` per definition - - Paginates via `pageInfo.endCursor` / `pageInfo.hasNextPage` until all definitions are collected - - Builds an in-memory map keyed by **`legacyIDV2`** (the value REST V3 will need) storing `label`, `dataType`, `dropDownOptions`, and `associations` -2. **Task 2 — Entity creation (REST V3)** — creates a transaction (e.g., `salesreceipt`, `invoice`, `bill`) or a `Customer` / `Vendor` / `Project` with a `CustomField` array attached. Sets `DefinitionId = legacyIDV2` from Task 1. Type-aware: chooses `StringValue` / `NumberValue` / `DateValue` based on each definition's `dataType`. **For `STRING_LIST` / `OBJECT_LIST` types, validates the written value against the definition's `dropDownOptions[]` before sending** — REST V3 will accept arbitrary strings, but they won't match QBO's UI dropdown and downstream features will break. **All requests must include `?include=enhancedAllCustomFields` in the query string** — without it, the response will not return the full custom-field metadata. -3. **Task 3 — Read & hydrate** — retrieves the created entity via REST V3 (with `?include=enhancedAllCustomFields`) and displays it with human-readable labels and type-correct value reads (using the cached definition map from Task 1). -4. **Task 4 (optional) — Create a new definition (GraphQL)** — calls `appFoundationsCreateCustomFieldDefinition` to programmatically provision a new custom field. Skip unless your app provisions fields as part of onboarding (e.g. an inventory app that requires a "SKU" field on every Invoice). Requires the write scope `app-foundations.custom-field-definitions` (no `.read` suffix). - -Plus: structured error handling (`401`, `400`, `403`, GraphQL errors on HTTP 200), `intuit_tid` observability logging, definition caching, and tier-awareness checks (see capability table below). - -## Capability by QBO Tier (Important) - -The Custom Fields API enforces hard limits per QBO product tier. Generated code that assumes "Vendor custom fields work everywhere" will break silently on Essentials. The prompt instructs the AI to surface tier-mismatch errors instead of letting empty GraphQL responses look like "no fields configured." - -| Tier | Max custom fields | Supported transactions | Supported entities | -|---|---|---|---| -| **Essentials** | 3 | SalesReceipt, Invoice, Estimate, CreditMemo, RefundReceipt | *(none — transactions only)* | -| **Plus** | 4 | SalesReceipt, Invoice, Estimate, CreditMemo, RefundReceipt, PurchaseOrder | *(none — transactions only)* | -| **Advanced / Intuit Enterprise Suite** | 12 | All of Plus + Purchase (Cash/Check/CC), Bill, Credit Card Payment, VendorCredit | Customer, Vendor, Projects | - -**Tier detection:** No GraphQL/REST field returns the QBO tier directly. Generated code should either (a) accept the tier as configuration, or (b) treat an empty Task 1 response on a known-configured field as a tier-mismatch hint and surface that to the developer. - -## The `legacyIDV2` Bridge (Non-Negotiable) - -The GraphQL response returns two ID-like fields per definition: - -| Field | Use | -|---|---| -| `id` | GraphQL internal ID (lowercase — the schema does NOT expose `Id`) — for GraphQL operations **only** | -| `legacyIDV2` | The value to pass as `DefinitionId` in **every** REST V3 call | - -Using the GraphQL `id` as `DefinitionId` in REST V3 will return **`400 Bad Request`**. The generated code enforces this bridge by keying the definition map on `legacyIDV2` everywhere. - -## GraphQL Filter Shape (Easy to Get Wrong) - -The `appFoundationsCustomFieldDefinitions` query takes an argument named **`filters`** (plural). The input type is `AppFoundations_CustomExtensionsDefinitionFilterBy` and its fields are **primitives** — there is no `{ equals: ... }` predicate wrapper: - -```graphql -appFoundationsCustomFieldDefinitions( - filters: { active: true, subType: "invoice" } # ✅ correct - first: 50 -) { edges { node { id legacyIDV2 label dataType active } } pageInfo { endCursor hasNextPage } } -``` - -```graphql -# ❌ Wrong — `filter` (singular) does not exist -appFoundationsCustomFieldDefinitions(filter: { ... }) -# ❌ Wrong — there is no `{ equals: }` wrapper on the filter fields -filters: { active: { equals: true }, subType: { equals: "invoice" } } -# ❌ Wrong — `subType` is only an input filter, NOT a node field -node { Id subType label } -``` - -The full `filters` input fields: `active: Boolean`, `subType: String`, `entityType: String`, `dataType: AppFoundations_CustomExtensionDataType` (enum), `ids: [ID]`. - -## Type-Aware Value Mapping - -Custom Field definitions have a `dataType` (from GraphQL) that determines which value field to populate on the REST V3 entity. The official `AppFoundations_CustomExtensionDataType` enum has these values: - -| GraphQL `dataType` | REST V3 `Type` | Value field to set | Format | Validation | -|---|---|---|---|---| -| `STRING` | `StringType` | `StringValue` | string | none (free text) | -| `NUMBER` | `NumberType` | `NumberValue` | decimal | numeric only | -| `DATE` | `DateType` | `DateValue` | `YYYY-MM-DD` | RFC3339 date | -| `STRING_LIST` | `StringType` | `StringValue` | must match an active `Value` from `dropDownOptions[]` (case-sensitive) | **strict — reject pre-write** | -| `OBJECT_LIST` | `StringType` | `StringValue` | must match an active `Value` from `dropDownOptions[]` | **strict — reject pre-write** | -| `UNKNOWN` | *(skip)* | *(skip)* | Skip with logged warning | always skip | - -> The published `AppFoundations_CustomExtensionDataType` enum does not list `BOOLEAN`. Older legacy-REST docs reference a `BooleanType` / `BooleanValue` pairing — confirm against the live schema if you see a boolean type returned. The generated code should treat any `dataType` outside the documented set as `UNKNOWN` and surface a warning. - -Every `CustomField` entry must populate **exactly one** value field. Supplying zero or more than one is a schema violation. - -The prompt instructs the AI to **read `dataType` from each definition** and choose the correct REST V3 `Type` and value field at write time, and to read the correct value field at hydration time in Task 3. Generated code that hardcodes `StringValue` for every field will not pass the type-aware reading check in Task 3. - -## How To Generate The Prompt - -From the `discover/` directory: - -```bash -# Default (uses prompt-config.json — integration_mode "new" scaffolds a sibling folder) -node merge-prompt.js -# then select choice 3 at the menu - -# Language-specific (selects the right sdk-notes/.md) -node merge-prompt.js --language java -node merge-prompt.js --language python -node merge-prompt.js --language dotnet -``` - -Output: `discover/generated-sample-prompts/custom-fields-ready-prompt.md` - -Copy that file's contents into your AI assistant. - -## Authentication & `.env` Setup - -The generated code assumes the caller already has a valid **OAuth 2.0 access token** and **realmId**: - -```bash -QBO_ACCESS_TOKEN= -QBO_REALM_ID= -QBO_MINOR_VERSION=75 -``` - -### Endpoints - -The Custom Fields GraphQL API is **production-only** — no sandbox is documented. - -| Surface | Host | -|---|---| -| GraphQL (Tasks 1, 4, 5) | `https://qb.api.intuit.com/graphql` | -| REST V3 (Tasks 2, 3) | `https://quickbooks.api.intuit.com` *(sandbox `https://sandbox-quickbooks.api.intuit.com` available for the entity payload itself, but the CF definitions still come from prod GraphQL)* | - -All REST V3 calls must include `?include=enhancedAllCustomFields` in the query string. - -Use the **[Intuit OAuth 2.0 Playground](https://developer.intuit.com/app/developer/playground)** to generate a production access token. For dev/test work, request a free non-expiring partner test account via the Intuit Partner Program rather than sandbox. - -## Required OAuth Scopes - -The generated code assumes the app is authorized with the scopes it needs. The `.read` and unsuffixed scopes are **different scope strings** — requesting `.read` does NOT grant write access. - -| Scope | Used For | Required for | -|---|---|---| -| `app-foundations.custom-field-definitions.read` | **Read** definitions via GraphQL | Task 1, Task 3 (hydration uses cache from Task 1) | -| `app-foundations.custom-field-definitions` | **Read + write** definitions via GraphQL (call `appFoundationsCreateCustomFieldDefinition`) | Task 4 only | -| `com.intuit.quickbooks.accounting` | Creating and reading transactions/entities via REST V3 | Task 2, Task 3 | - -A `403 Forbidden` on the GraphQL query usually means `app-foundations.custom-field-definitions.read` is missing, Custom Fields are not enabled at the company level, or the app is not on a Silver/Gold/Platinum partner tier. A `403` on the REST V3 calls usually means `com.intuit.quickbooks.accounting` is missing. A `403` on the create-definition mutation means the unsuffixed `app-foundations.custom-field-definitions` scope is missing (the `.read` scope alone won't work for writes). - -## Configuration - -All values are sourced from `discover/prompt-config.json` (or `prompt-config-existing.json` for drop-in mode). Custom-fields-specific keys: - -| Key | What it controls | Default | -|---|---|---| -| `type_of_transaction` | Transaction type the custom fields attach to (also passed as GraphQL `subType` filter) | `salesreceipt` | -| `custom_field_documentation` | Docs URL injected into the prompt | Custom Fields workflow page | -| `custom_field_scope_read` | OAuth scope for Task 1/3 (read) | `app-foundations.custom-field-definitions.read` | -| `custom_field_scope_readwrite` | OAuth scope for Task 4 (write) | `app-foundations.custom-field-definitions` | -| `custom_field_definitions_query` | GraphQL query for fetching active definitions (includes `dropDownOptions`, `associations`, cursor variable) | `query GetCustomFieldDefinitions($cursor: String) { ... }` | -| `custom_field_create_definition_mutation` | GraphQL mutation for Task 4 | `mutation CreateCustomFieldDefinition($input: ...) { ... }` | -| `custom_field_create_definition_variables_example` | Example variables JSON for Task 4 | A `STRING` definition associated with `"transaction"` | -| `custom_field_payload_structure` | JSON shape of the `CustomField` array in the REST payload (string-typed example) | `[ { "DefinitionId": ..., "Type": "StringType", "StringValue": ... } ]` | -| `custom_field_transaction_creation_instructions` | Free-form instructions for Task 2 | Defaults to creating with item id 1, customer 1, amount 111 | -| `minorversion` | REST V3 minor version | `75` | - -Shared keys (also used by Dimensions, Projects, and Sales Tax prompts): - -- `language_framework`, `typing_system`, `integration_mode` -- `graphql_endpoint_production`, `graphql_schema` (the Custom Fields GraphQL API has no sandbox; `graphql_endpoint_sandbox` is unused here) -- `rest_baseurl_production`, `rest_baseurl_sandbox` (REST V3 sandbox is fine for Tasks 2/3 entity payloads) -- `transaction_v3_api_endpoint`, `get_transaction_endpoint`, `rest_v3_api_documentation` -- `java_sdk_version`, `java-sdk-documentation`, `oauth2-documentation`, `php-sdk-documentation` -- `custom_field_sample_app_java`, `custom_field_sample_app_python` - -## Supported Entities - -Custom Fields support varies by entity. Confirmed entities for use with this prompt: - -**Transactions:** `Invoice` · `SalesReceipt` · `Estimate` · `CreditMemo` · `RefundReceipt` · `Bill` · `Purchase` · `VendorCredit` · `PurchaseOrder` · `JournalEntry` - -**Entities:** `Customer` · `Vendor` - -Always confirm support for your specific entity against the [Custom Fields API documentation](https://developer.intuit.com/app/developer/qbo/docs/workflows/create-custom-fields) before generating code. - -## Integration Mode (`new` vs. `existing`) - -Same as Dimensions, Projects, and Sales Tax — controlled by `integration_mode` in the config: - -| Mode | What the AI produces | -|---|---| -| `new` | Self-contained folder `custom-fields-/` with README, dependency manifest, and architecture diagram showing GraphQL discovery → legacyIDV2 bridge → REST creation → hydration | -| `existing` | Drop-in modules with integration notes — no new folder created | - -Use `prompt-config-existing.json` (or set `integration_mode: "existing"` in your config) when pasting the prompt into an existing codebase. - -## Anti-Hallucination Guardrails - -The prompt enforces these hard rules on the AI: - -1. **No invented endpoints, GraphQL field names, or `DefinitionId`s** — derive everything from provided docs and the live query -2. **`legacyIDV2` is the REST bridge** — never use the GraphQL `id` as `DefinitionId` -3. **Production-only GraphQL** — the Custom Fields GraphQL API has no sandbox; don't generate a `QBO_ENV=sandbox` branch for Tasks 1, 4, or 5. REST V3 in Tasks 2/3 still has a sandbox option, but the CF discovery hits production GraphQL either way. -4. **GraphQL filter shape** — argument is `filters` (plural) with primitive fields (no `{ equals: }` wrappers); node fields are lowercase `id` and don't include `subType` -5. **REST URL must include `&include=enhancedAllCustomFields`** on every create/read of an entity with custom fields -6. **Provided links only** — no external API references -7. **GraphQL errors on 200** — always inspect the `errors` array, not just the HTTP status -8. **Strict SDK usage** — SDKs handle REST (Tasks 2 & 3) only; Task 1 GraphQL discovery uses a plain HTTP client because no official SDK supports GraphQL today -9. **Type-aware value mapping** — read `dataType` from GraphQL, write the matching value field in REST V3; the published enum does not include `BOOLEAN`, but the AI should follow the live schema if it differs -10. **Stop if blocked** — state what's missing instead of guessing - -These are enforced by the "🛑 AI Guardrails" section at the bottom of the template. - -## Per-Language SDK Notes (`sdk-notes/`) - -Language-specific SDK guidance injected into the template via the `{{sdk_notes}}` placeholder. Tells the AI exactly which SDK classes/methods to use so it doesn't hallucinate fake method signatures. - -**No SDK supports GraphQL today** — every language note directs Task 1 (GraphQL discovery) to a plain HTTP client. SDK-backed languages use the SDK for Tasks 2 & 3 (REST V3 creation and read). - -| Language | File | Strategy | -|---|---|---| -| Java | `sdk-notes/java.md` | Task 1: Apache HttpClient. Tasks 2 & 3: `ipp-v3-java-devkit` (`DataService.add()` + `CustomField` model) | -| .NET | `sdk-notes/dotnet.md` | Task 1: `System.Net.Http.HttpClient`. Tasks 2 & 3: `IppDotNetSdkForQuickBooksApiV3` (`DataService.Add()` + `CustomField` model) | -| PHP | `sdk-notes/php.md` | Task 1: `GuzzleHttp\Client`. Tasks 2 & 3: `quickbooks/v3-php-sdk` (`DataService::Add()` + `IPPCustomField` model) | -| Node.js | `sdk-notes/nodejs.md` | Plain HTTP throughout — `axios` or native `fetch` (no official Node SDK) | -| Python | `sdk-notes/python.md` | Plain HTTP throughout — `requests` (no official Python SDK) | -| Ruby | `sdk-notes/ruby.md` | Plain HTTP throughout — `Net::HTTP` or `faraday` (community gem coverage uneven) | - -**Fallback:** Any language without a matching file falls back to a generic "use plain HTTP" message generated by `merge-prompt.js:loadSdkNotes()`. Unsupported languages (Rust, Go, Kotlin, etc.) still produce valid prompts. - -**To add a new language:** -1. Create `sdk-notes/.md` following the structure of the existing files (Task 1 GraphQL → Task 2 REST create → Task 3 REST read → install) -2. Run `node merge-prompt.js --language ` from `discover/` - -## File Structure - -``` -custom-fields/ -├── README.md # This file -├── prompt-template-custom-fields.md # The template — {{placeholder}} tokens get substituted at merge time -└── sdk-notes/ # Per-language SDK guidance injected via {{sdk_notes}} - ├── java.md - ├── dotnet.md - ├── php.md - ├── nodejs.md - ├── python.md - └── ruby.md -``` - -## Common Errors - -| Code | Most Likely Cause | -|---|---| -| `400` on GraphQL with `GRAPHQL_VALIDATION_FAILED` | Selecting fields that don't exist on the schema. Common mistakes: `Id` / `Value` (capital) on `dropDownOptions` — the schema uses lowercase `id` / `value`; or using `filter` (singular) instead of `filters` (plural); or wrapping primitive filter fields in `{ equals: ... }` predicates | -| `400` on REST V3 | Using the GraphQL `id` instead of `legacyIDV2` as `DefinitionId`, populating the wrong value field for the `dataType`, omitting `?include=enhancedAllCustomFields` from the URL, or exceeding the entity's custom-field cap (see tier table) | -| `401` (XML body) | Expired access token — refresh in the [OAuth Playground](https://developer.intuit.com/app/developer/playground). Note: gateway returns XML for auth failures, not JSON. | -| `403` on GraphQL | Missing `app-foundations.custom-field-definitions.read` scope, Custom Fields disabled on the company, or app is on Builder tier (premium API requires Silver+) | -| `403` on REST V3 | Missing `com.intuit.quickbooks.accounting` scope | -| **`200` with `data: null` and `errors[].extensions.errorCode.errorCode = "AUTHORIZATION_DENIED"`** | This is what a missing **write** scope (Task 4) looks like in practice — NOT a clean 403. The `errors[].message` is `"access denied"`, `extensions.service` is `"custom-extensions-service"`. Re-auth the app with the unsuffixed `app-foundations.custom-field-definitions` scope. | -| `200` with other `errors` | GraphQL partial failure — always inspect the response body, even on HTTP 200 | -| Empty definitions response when you expected results | Most likely a **tier mismatch** (your target entity isn't supported on this QBO tier — see capability table). Less common: actually no definitions configured. Empirically, `subType` and `entityType` filters are unreliable for narrowing — the prompt fetches all active definitions and filters client-side. | -| Custom field values come back wrong on read | Generated code is reading `StringValue` for every field. Verify Task 3's type-aware reading branch is actually consulting the cached `dataType` before choosing a value field | -| Custom field metadata missing on REST V3 read | The URL is missing `?include=enhancedAllCustomFields` — add it to every create and read. Without the flag, the API returns only a legacy `DefinitionId: "1"` placeholder (mapped to the legacy 3-string-fields slot) with no `StringValue`. Verified in production. | -| HTTP 200 on create but `CustomField` array empty or missing entries | The `DefinitionId` isn't associated with the target entity type (e.g., a definition created for `Invoice` was sent on a `SalesReceipt`). REST V3 silently drops mismatched entries with no error. Use a `DefinitionId` whose definition's `associations[].associatedEntity` matches the target entity (either the specific path like `/transactions/Invoice` or the generic `/transactions/Transaction`). | - -### Empirical quirks (verified against production, May 2026) - -| Behavior | Implication | -|---|---| -| `dropDownOptions { Id Value ... }` — wrong case | Schema rejects with `GRAPHQL_VALIDATION_FAILED`. Use lowercase `id` and `value`. The doc's section headers misled prompt authors. | -| `subType: "salesreceipt"` returns 0 results even when matching definitions exist | The server-side `subType` filter is too narrow. Fetch all active definitions (`filters: { active: true }`) and filter client-side on `associations[].associatedEntity`. | -| `entityType: "transaction"` returns 0 results | Schema expects path-style values like `"/transactions/Transaction"` — not the simple `"transaction"` shown in the doc. Same workaround: filter client-side. | -| `associations[].associatedEntity` returns path-style strings | Values look like `"/transactions/Transaction"`, `"/transactions/Invoice"`, `"/transactions/SalesReceipt"`, `"/Customer"`, `"/Vendor"`. The doc's plain-word examples (`"transaction"`, `"customer"`) are conceptual only — actual values are paths. | -| `dropDownOptions: []` (empty array) for non-list types | Don't treat as missing data. Only validate against `dropDownOptions` when `dataType` is `STRING_LIST` or `OBJECT_LIST`. | -| Task 4 (create) without write scope returns HTTP 200 with `errors[].extensions.errorCode.errorCode = "AUTHORIZATION_DENIED"` | Not a gateway 403. Match on the `errorCode` and `service: "custom-extensions-service"` to detect this case. | - -## Empty State - -If no active definitions exist for the configured `type_of_transaction`, the generated code surfaces: - -> *"No active Custom Field definitions found for ``. Please configure them in QuickBooks settings."* - -…and halts cleanly without attempting the REST create. - -## Reference Materials - -Official artifacts to crib from when building your integration: - -- **Java / Spring Boot sample app:** https://github.com/IntuitDeveloper/SampleApp-CustomFields-Java -- **Python sample app:** https://github.com/IntuitDeveloper/SampleApp-CustomFields-Python -- **Premium APIs Postman workspace:** https://www.postman.com/intuit-developer/intuit-developer-premium-apis/overview -- **GraphQL schema reference:** [appFoundationsCustomFieldDefinitions](https://developer.intuit.com/app/developer/gql/docs/api/qbexternal/queries/appFoundationsCustomFieldDefinitions) · [Create mutation](https://developer.intuit.com/app/developer/gql/docs/api/qbexternal/mutations/appFoundationsCreateCustomFieldDefinition) · [Update mutation](https://developer.intuit.com/app/developer/gql/docs/api/qbexternal/mutations/appFoundationsUpdateCustomFieldDefinition) - -The sample apps use `ipp-v3-java-devkit` (Java) and `requests` (Python) for REST V3, and plain HTTP for the GraphQL operations — same split documented in the per-language SDK notes. - -## Related - -- Parent docs: [`../README.md`](../README.md) -- Template source: [`prompt-template-custom-fields.md`](./prompt-template-custom-fields.md) -- Merge script: [`../merge-prompt.js`](../merge-prompt.js) -- Generated output: [`../generated-sample-prompts/custom-fields-ready-prompt.md`](../generated-sample-prompts/) diff --git a/discover/custom-fields/prompt-template-custom-fields.md b/discover/custom-fields/prompt-template-custom-fields.md index a0de6d4..973cbb8 100644 --- a/discover/custom-fields/prompt-template-custom-fields.md +++ b/discover/custom-fields/prompt-template-custom-fields.md @@ -140,7 +140,7 @@ Invalidate the Task 1 cache after a successful update. --- -## Technical Best Practices +## Technical Best Practices: - **Caching:** Cache the Task 1 definition map for 1 hour. Invalidate after Task 4 or Task 5. - **Error Handling:** Include blocks for: @@ -157,17 +157,11 @@ Invalidate the Task 1 cache after a successful update. --- -## Language-Specific SDK Notes - -{{sdk_notes}} - ---- - ## 🛑 AI Guardrails (Anti-Hallucination Constraints) **CRITICAL INSTRUCTIONS - YOU MUST ADHERE TO THE FOLLOWING:** 1. **No Hallucinations:** Do not invent, guess, or hallucinate API endpoints, GraphQL properties, or SDK methods that are not explicitly provided in the context or linked documentation. -2. **Strict SDK/Library Usage:** If an official SDK or library is specified (e.g., Intuit Java SDK), use ONLY the methods and classes that exist in its latest public release. Do not construct fake SDK models. For Custom Fields specifically: GraphQL (Tasks 1, 4, 5) has no SDK — use a plain HTTP client. For REST V3 (Tasks 2 & 3), use the SDK's create/read methods (`DataService.add()` / `findById()` in Java, `dataService.Add()` in .NET, `entity.save(qb=client, params=...)` in Python). Each SDK has a language-specific way to attach the `enhancedAllCustomFields` include parameter; see the language-specific SDK notes for the exact mechanism. (Note: the Node.js SDK has no public hook for this parameter — for Node, build the request body as a plain object with PascalCase keys and POST via plain HTTP with `&include=enhancedAllCustomFields` on the URL.) +2. **Strict SDK/Library Usage:** If an official SDK or library is specified (e.g., Intuit Java SDK), use ONLY the methods and classes that exist in its latest public release. Do not construct fake SDK models. For Custom Fields specifically: GraphQL (Tasks 1, 4, 5) has no SDK — use a plain HTTP client. For REST V3 (Tasks 2 & 3), the deciding constraint is the mandatory `include=enhancedAllCustomFields` URL parameter: use the SDK's create/read methods (`DataService.add()` / `findById()` in Java, `dataService.Add()` in .NET) **only when the SDK exposes a public hook to attach that parameter**. If it does not — as with the Python and Node.js SDKs — do not force it through the SDK: build the request body as a plain object with PascalCase keys and POST via plain HTTP with `&include=enhancedAllCustomFields` on the URL. 3. **Provided Links Only:** You must derive all API syntax, structure, and constraints strictly from the provided links. All HTTP responses (GraphQL and REST) must be parsed according to the provided documentation. 4. **Endpoint Strictness:** Use the exact endpoints and query structures provided. Do not attempt to modify the base URL, append unsupported parameters, or alter the `minorversion={{minorversion}}` requirement. 5. **If Blocked/Missing Info:** If the provided documentation or payload structures lack required fields to compile a functional request, STOP and clearly state what specific information is missing instead of making an educated guess. diff --git a/discover/custom-fields/sdk-notes/dotnet.md b/discover/custom-fields/sdk-notes/dotnet.md deleted file mode 100644 index a3ff1e7..0000000 --- a/discover/custom-fields/sdk-notes/dotnet.md +++ /dev/null @@ -1,90 +0,0 @@ -**If generating .NET / C# code (language: `{{language_framework}}`):** - -Use **`System.Net.Http.HttpClient`** for Task 1 (GraphQL discovery — no SDK supports GraphQL) and the official **`IppDotNetSdkForQuickBooksApiV3`** for Tasks 2 & 3 (REST V3). - -- **Task 1 — GraphQL discovery (plain HTTP):** - - The Intuit .NET SDK does **not** support GraphQL. Use `System.Net.Http.HttpClient` (part of the .NET standard library — no extra package needed) to POST the `appFoundationsCustomFieldDefinitions` query to `{{graphql_endpoint_production}}` (or `{{graphql_endpoint_sandbox}}` when `QBO_ENV=sandbox`). - - Send the argument as **`filters`** (plural — `AppFoundations_CustomExtensionsDefinitionFilterBy` input type) with primitive fields: `{ active: true }`. Do not use `{ equals: ... }` predicate wrappers. Filter by target entity client-side after the response — see the main prompt's Task 1 for the `associations[].associatedEntity` + `subAssociations[].associatedEntity` filtering logic. - - Parse the JSON response and build a `Dictionary` keyed by `legacyIDV2` (NOT the GraphQL `id`). Store `label` and `dataType` per entry. The GraphQL node field is lowercase `id`. - - Handle pagination via `pageInfo.endCursor` / `pageInfo.hasNextPage`. - -- **Task 2 — REST V3 entity creation (SDK):** - - ### Setup — build ServiceContext, add include param, construct DataService - - ```csharp - using Intuit.Ipp.Core; - using Intuit.Ipp.Security; - using Intuit.Ipp.DataService; - using Intuit.Ipp.Data; - - string raw = token.StartsWith("Bearer ") ? token.Substring(7) : token; - var validator = new OAuth2RequestValidator(raw); - var serviceContext = new ServiceContext(realmId, IntuitServicesType.QBO, validator); - serviceContext.IppConfiguration.MinorVersion.Qbo = "{{minorversion}}"; - - // Add the include parameter BEFORE constructing DataService so every call uses it. - // Public, supported API on ServiceContext.Include — verified in the SDK's IncludeParamTest.cs. - serviceContext.Include.Add("enhancedAllCustomFields"); - - var dataService = new DataService(serviceContext); - ``` - - ### Build the entity with typed POJOs - - - The entity class matching `{{type_of_transaction}}` from the `Intuit.Ipp.Data` namespace (e.g., `Invoice`, `SalesReceipt`, `Estimate`, `Bill`, `Customer`, `Vendor`) — the entity object - - `Intuit.Ipp.Data.CustomField` — one instance per custom field - - `DefinitionId` — pass the **`legacyIDV2`** from Task 1, **not** the GraphQL `id` - - `Name` — the `label` from Task 1 (recommended for readability) - - `Type` — map Task 1's `dataType`: `STRING` / `STRING_LIST` / `OBJECT_LIST` → `CustomFieldTypeEnum.StringType`, `NUMBER` → `CustomFieldTypeEnum.NumberType`, `DATE` → `CustomFieldTypeEnum.DateType`. The published enum does not list `BOOLEAN`; if the live schema returns one, follow the schema. Skip `UNKNOWN` with a logged warning. - - `AnyIntuitObject` (typed as `object`) — the value to attach. Verified against the .NET SDK's compiled `Intuit.Ipp.Data.dll`: `CustomField` exposes exactly four properties (`DefinitionId`, `Name`, `Type`, `AnyIntuitObject`). There are **no** typed `StringValue` / `NumberValue` / `DateValue` properties — the XML-choice serialization picks the right element name based on the runtime type you assign: - - `string` → serializes as `` (for `STRING` / `STRING_LIST` / `OBJECT_LIST`) - - `decimal` → serializes as `` (for `NUMBER`) - - `DateTime` → serializes as `` (for `DATE`, value is `YYYY-MM-DD`) - - `bool` → serializes as `` (rarely surfaced) - - Set exactly one runtime type per `CustomField` (the XSD declares this as `xs:choice` with `minOccurs="1"`). Example: - ```csharp - var cf = new CustomField { - DefinitionId = definition.LegacyIDV2, // legacyIDV2, NOT GraphQL id - Name = definition.Label, - Type = CustomFieldTypeEnum.StringType, - AnyIntuitObject = "Demo value" // string → StringValue element - }; - ``` - - Attach the `CustomField[]` array to the entity's `CustomField` property, then call `dataService.Add(entity)`. Because `Include.Add("enhancedAllCustomFields")` was set on the ServiceContext at setup time, the response will carry the full CustomField metadata for round-trip verification. - - ### Silent-drop detection - - REST V3 silently drops `CustomField` entries whose `DefinitionId` doesn't match the target entity's sub-association exactly. After `dataService.Add(...)` returns, compare the response's `CustomField` array against your sent list and warn on any missing entries. Most common cause: a `SALE_INVOICE`-scoped definition will not attach to a SalesReceipt (`SALE`-scoped) — and vice versa. - - ### Error handling - - `DataService.Add()` throws on API failures (the SDK's exception hierarchy lives under `Intuit.Ipp.Exception`). Inspect `e.Message` and any inner exception. For `intuit_tid` correlation, enable the SDK's request/response logger on `serviceContext.IppConfiguration.Logger.RequestLog` (writes to disk or stream) — the SDK does not expose `intuit_tid` as a direct exception-getter property. - -- **Task 3 — Type-aware hydration (SDK):** - - Reuse the same `serviceContext` (with `Include.Add("enhancedAllCustomFields")` already applied) and `DataService` from Task 2. Build a probe with the Id set: - ```csharp - var probe = new SalesReceipt { Id = createdId }; - var fetched = dataService.FindById(probe); - ``` - - The REST response's `DefinitionId` equals the `legacyIDV2` you stored in Task 1 — use it to look up `label` and `dataType` from your cached map. - - Use the cached `dataType` to cast `AnyIntuitObject` to the right runtime type. **Reminder:** there are no typed getters like `cf.StringValue` on the SDK class — read `cf.AnyIntuitObject` and cast: - ```csharp - object value = rcf.AnyIntuitObject; - object typed = meta.DataType switch { - "STRING" or "STRING_LIST" or "OBJECT_LIST" => (string)value, - "NUMBER" => (decimal)value, - "DATE" => (DateTime)value, - _ => null - }; - ``` - - Filter the entity's `Line` array to entries where `DetailType == LineDetailTypeEnum.SalesItemLineDetail`. Exclude `SubTotalLineDetail`, `DiscountLineDetail`, `TaxLineDetail`. - -- **NuGet install:** - ``` - Install-Package IppDotNetSdkForQuickBooksApiV3 - ``` -- **SDK reference:** `{{dotnet-sdk-documentation}}` - -> Warning: Use **only** methods and classes that exist in the published SDK. Do not construct fake SDK models or invent method signatures. diff --git a/discover/custom-fields/sdk-notes/java.md b/discover/custom-fields/sdk-notes/java.md deleted file mode 100644 index bc581aa..0000000 --- a/discover/custom-fields/sdk-notes/java.md +++ /dev/null @@ -1,234 +0,0 @@ -**If generating Java code (language: `{{language_framework}}`):** - -Use the **Intuit Java SDK** (`ipp-v3-java-devkit`) **v{{java_sdk_version}}** for Tasks 2 and 3, and **Apache HttpClient + Jackson** for Task 1 (the SDK has no GraphQL support). The SDK supports `enhancedAllCustomFields` via a public method on `Context` — verified live against production with end-to-end round-trip of Invoice, SalesReceipt, and Customer custom-field writes. - -> **SDK version:** `6.7.0` is the latest. `6.5.2` / `6.5.3` work identically for custom fields (same `setIncludeParam` API, same typed CustomField setters). Prefer the latest unless your project pins an earlier version. - ---- - -## Task 1 — GraphQL discovery (plain HTTP + Jackson) - -The Intuit Java SDK does not support GraphQL. Use `CloseableHttpClient` from Apache HttpClient (a transitive dependency of `ipp-v3-java-devkit`) to POST the `appFoundationsCustomFieldDefinitions` query to `{{graphql_endpoint_production}}` (production only — GraphQL has no sandbox endpoint). - -- Build the request body as a `Map` with keys `query` and `variables`. Serialize with `objectMapper.writeValueAsString(body)`. -- Send the argument as **`filters`** (plural — `AppFoundations_CustomExtensionsDefinitionFilterBy` input type) with primitive fields: `{ active: true }`. Do not use `{ equals: ... }` predicate wrappers. Filter by target entity client-side after the response. -- Parse with Jackson (`objectMapper.readTree(body)` then walk via `JsonNode`). Build a `Map` keyed by `legacyIDV2` (NOT the GraphQL `id`). Store `label` and `dataType` per entry. The GraphQL node field is lowercase `id`. -- Handle pagination via `pageInfo.endCursor` / `pageInfo.hasNextPage`. - ---- - -## Task 2 — REST V3 entity creation (SDK) - -The SDK's `DataService.add()` handles serialization correctly when the Context has the include parameter set. Hold the `Context` reference yourself so you can call `setIncludeParam` on it directly — `DataService.getContext()` is private in the SDK, so reflection is the only alternative, and it isn't necessary if you keep the reference. - -### Setup - -```java -import com.intuit.ipp.core.Context; -import com.intuit.ipp.core.ServiceType; -import com.intuit.ipp.security.OAuth2Authorizer; -import com.intuit.ipp.services.DataService; - -String raw = token.startsWith("Bearer ") ? token.substring(7) : token; -Context context = new Context(new OAuth2Authorizer(raw), ServiceType.QBO, realmId); -context.setMinorVersion("{{minorversion}}"); - -// Set BEFORE constructing DataService so every call on this DataService uses it. -context.setIncludeParam(java.util.List.of("enhancedAllCustomFields")); - -DataService dataService = new DataService(context); -``` - -### Build the entity with typed POJOs - -```java -import com.intuit.ipp.data.SalesReceipt; // or Invoice / Estimate / Bill / Customer / Vendor — match {{type_of_transaction}} -import com.intuit.ipp.data.Line; -import com.intuit.ipp.data.LineDetailTypeEnum; -import com.intuit.ipp.data.SalesItemLineDetail; -import com.intuit.ipp.data.ReferenceType; -import com.intuit.ipp.data.CustomField; -import com.intuit.ipp.data.CustomFieldTypeEnum; -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.List; - -SalesReceipt sr = new SalesReceipt(); - -ReferenceType custRef = new ReferenceType(); -custRef.setValue("1"); -sr.setCustomerRef(custRef); - -Line line = new Line(); -line.setAmount(new BigDecimal("111")); -line.setDetailType(LineDetailTypeEnum.SALES_ITEM_LINE_DETAIL); -SalesItemLineDetail sild = new SalesItemLineDetail(); -ReferenceType itemRef = new ReferenceType(); -itemRef.setValue("1"); -sild.setItemRef(itemRef); -line.setSalesItemLineDetail(sild); -sr.getLine().add(line); - -CustomField cf = new CustomField(); -cf.setDefinitionId(definition.legacyIDV2()); // legacyIDV2 from Task 1, NOT the GraphQL id -cf.setName(definition.label()); // optional but recommended -cf.setType(CustomFieldTypeEnum.STRING_TYPE); // STRING_TYPE / NUMBER_TYPE / DATE_TYPE based on Task 1's dataType -cf.setStringValue("Demo value"); // pick exactly ONE typed setter (see below) -// Use a mutable list if downstream code may add fields. -sr.setCustomField(new ArrayList<>(List.of(cf))); - -SalesReceipt created = dataService.add(sr); -String createdId = created.getId(); -``` - -### Typed value setters on `CustomField` - -Verified against `ipp-v3-java-data:{{java_sdk_version}}`: - -- `setStringValue(String)` — for `STRING_TYPE` (used when Task 1's `dataType` is `STRING`, `STRING_LIST`, or `OBJECT_LIST`) -- `setNumberValue(BigDecimal)` — for `NUMBER_TYPE` (used when `dataType` is `NUMBER`) -- `setDateValue(java.util.Date)` — for `DATE_TYPE` (used when `dataType` is `DATE`) -- `setBooleanValue(Boolean)` — for `BooleanType` (rarely surfaced by the GraphQL schema) - -Set **exactly one** typed value field per `CustomField`. There is no `setAnyIntuitObject(Object)` method on `CustomField` — do not call one. - -For `STRING_LIST` / `OBJECT_LIST`, validate `StringValue` against the definition's active `dropDownOptions[].value` set (from Task 1) before sending. Mismatched values are rejected by the API. - -### Silent-drop detection - -REST V3 silently drops `CustomField` entries whose `DefinitionId` doesn't match the target entity's sub-association exactly. After `dataService.add(...)` returns, compare `created.getCustomField()` against your sent list and warn on any missing entries. Most common cause: a `SALE_INVOICE`-scoped definition will not attach to a SalesReceipt (`SALE`-scoped) — and vice versa. - -### Error handling — `FMSException` - -`DataService.add()` throws `com.intuit.ipp.exception.FMSException` on API failures. Important behaviors: - -- `e.getErrorList()` — list of `com.intuit.ipp.data.Error` objects with code, message, detail. Code `6240` = duplicate name (most common on Customer creation). -- `e.getIntuit_tid()` — populated on failed calls. Log this for tracing alongside the `intuit_tid` you set on outbound requests. -- **Do NOT call `DataService.getLastRequestId()`** — that method does not exist in SDK 6.7.0. If you need the request id, use the `intuit_tid` you generated and set on the outbound request (the SDK forwards it). - ---- - -## Task 3 — Type-aware hydration (SDK) - -Reuse the same `Context` (with `setIncludeParam` already applied) and `DataService` from Task 2. The SDK's `findById` takes an entity "probe" with the Id set: - -```java -SalesReceipt probe = new SalesReceipt(); -probe.setId(createdId); -SalesReceipt fetched = dataService.findById(probe); - -for (CustomField rcf : fetched.getCustomField()) { - String defId = rcf.getDefinitionId(); // equals legacyIDV2 - DefinitionMeta meta = definitionMap.get(defId); - Object value = switch (meta.dataType()) { - case "STRING", "STRING_LIST", "OBJECT_LIST" -> rcf.getStringValue(); - case "NUMBER" -> rcf.getNumberValue(); - case "DATE" -> rcf.getDateValue(); - default -> null; - }; - // ...render label + value for the UI... -} -``` - -There is no `getAnyIntuitObject()` method on `CustomField` — use the four typed getters (`getStringValue`, `getNumberValue`, `getDateValue`, `isBooleanValue`). - -Filter the response's `Line` list to entries where `getDetailType() == LineDetailTypeEnum.SALES_ITEM_LINE_DETAIL`. Exclude system-generated lines (`SubTotalLineDetail`, `DiscountLineDetail`, `TaxLineDetail`). - ---- - -## Dependencies - -```xml - - com.intuit.quickbooks-online - ipp-v3-java-devkit - {{java_sdk_version}} - - - -``` - -Add `oauth2-platform-api:{{java_sdk_version}}` separately if you need to implement OAuth refresh (this prompt assumes the token in `.env` is already valid). - ---- - -## Required runtime essentials (don't forget these — they break silently) - -- **SLF4J + a binding.** `pom.xml` must declare both `org.slf4j:slf4j-api` AND a binding (`slf4j-simple` or `logback-classic`). Without a binding, `log.info` / `log.warn` calls silently no-op at runtime, which breaks the `intuit_tid` observability requirement. -- **`intuit_tid` per request.** Generate a new UUID and either pass it via `RequestElements` on the SDK call, or — when bypassing the SDK for Task 1 — set it as the `intuit_tid` request header. Log the response `intuit_tid` for log correlation with Intuit support. -- **`.env` loading robustness.** Use `io.github.cdimascio:dotenv-java`; configure `Dotenv.configure().directory().ignoreIfMissing().load()`. Don't rely on `Path.of(".env")` — that only resolves when cwd happens to be the project root. - ---- - -## Alternative: no-SDK approach (Map + plain HTTP) - -Use this only if you don't want the Intuit SDK dependency on your classpath (e.g. GraalVM native-image targets, projects already invested in `RestTemplate`-style HTTP). It's a valid working path that's been verified live against the same realm, but the SDK path above is simpler and gets you typed entities. - -Build the request body as a nested `Map` with PascalCase keys typed in directly, then POST via `CloseableHttpClient` or `java.net.http.HttpClient` with `&include=enhancedAllCustomFields` on the URL. **Do not** use the SDK's data classes plus Jackson with `JaxbAnnotationModule` — that produces camelCase JSON (`stringValue`, `customField`) which QBO REST V3 rejects. - -### Build the body - -```java -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import java.util.*; - -ObjectMapper mapper = new ObjectMapper(); -Map body = new HashMap<>(); - -List> lines = new ArrayList<>(); -Map line = new HashMap<>(); -line.put("Amount", 111.00); -line.put("DetailType", "SalesItemLineDetail"); -Map sild = new HashMap<>(); -Map itemRef = new HashMap<>(); -itemRef.put("value", "1"); -sild.put("ItemRef", itemRef); -line.put("SalesItemLineDetail", sild); -lines.add(line); -body.put("Line", lines); - -Map customerRef = new HashMap<>(); -customerRef.put("value", "1"); -body.put("CustomerRef", customerRef); - -List> customFields = new ArrayList<>(); -Map cf = new HashMap<>(); -cf.put("DefinitionId", definition.legacyIDV2()); -cf.put("Name", definition.label()); // optional but recommended -cf.put("Type", "StringType"); -cf.put("StringValue", "Demo value"); -customFields.add(cf); -body.put("CustomField", customFields); - -String requestJson = mapper.writeValueAsString(body); -``` - -POST to `https://quickbooks.api.intuit.com/v3/company//{{type_of_transaction}}?minorversion={{minorversion}}&include=enhancedAllCustomFields` (sandbox host when `QBO_ENV=sandbox`). - -Parse the response: `JsonNode entity = mapper.readTree(body).get("");` — e.g. `"SalesReceipt"` when `{{type_of_transaction}}` is `salesreceipt`. **QBO wraps the response in this top-level key; the request body must NOT have such a wrapper.** - -Hydrate by switching on `dataType` and reading `rcf.get("StringValue").asText()` / `.decimalValue()` / `.asText()` for `STRING` / `NUMBER` / `DATE`. - -### Alternative-path dependencies - -```xml - - com.fasterxml.jackson.core - jackson-databind - 2.17.1 - - - org.apache.httpcomponents - httpclient - 4.5.14 - -``` - -Or use `java.net.http.HttpClient` (built into JDK 11+) and skip the Apache dependency. diff --git a/discover/custom-fields/sdk-notes/nodejs.md b/discover/custom-fields/sdk-notes/nodejs.md deleted file mode 100644 index 28c339a..0000000 --- a/discover/custom-fields/sdk-notes/nodejs.md +++ /dev/null @@ -1,179 +0,0 @@ -**If generating Node.js code (language: `{{language_framework}}`):** - -There is no Intuit-published entity SDK for Node.js. The official Intuit Custom Fields Node sample ([`IntuitDeveloper/Sampleapp-Customfields-Nodejs`](https://github.com/IntuitDeveloper/Sampleapp-Customfields-Nodejs)) uses **`graphql-request`** for GraphQL calls (Tasks 1, 4, 5) and **plain HTTP** (`axios` or native `fetch`) for everything else. There is also a community library `node-quickbooks` (by mcohen01) — **do not use it for this prompt** — its `module.request` honors only two hardcoded entity pseudo-fields (`allowDuplicateDocNum`, `requestId`) and exposes no public hook for adding query parameters like `include=enhancedAllCustomFields`. To send `include=enhancedAllCustomFields` via `node-quickbooks`, you'd need to fork or monkey-patch it. - -> **Note about the official Intuit Node sample:** that sample only implements Tasks 1, 4, and 5 (GraphQL definition CRUD). It does NOT implement Tasks 2 or 3 (creating an Invoice / SalesReceipt with attached custom-field values via REST V3). That's why no entity SDK appears in its `package.json` — those tasks need plain HTTP. - -## HTTP clients - -- **`graphql-request`** (recommended for Task 1 / 4 / 5) — `npm install graphql-request graphql`. Concise, handles the `query`/`variables` envelope automatically. -- **`axios`** (recommended for Tasks 2 / 3) — `npm install axios`. Used by the official Intuit sample. -- **native `fetch`** (Node 18+) — viable alternative to axios; no extra dep needed. -- **`intuit-oauth`** (optional) — `npm install intuit-oauth`. OAuth token flow only (login / exchange / refresh). NOT an entity SDK. Use only if you need to implement OAuth refresh; this prompt assumes the token in `.env` is already valid. - ---- - -## Task 1 — GraphQL discovery (`graphql-request`) - -```js -import { GraphQLClient, gql } from 'graphql-request'; - -const GRAPHQL_URL = 'https://qb.api.intuit.com/graphql'; // production-only - -const client = new GraphQLClient(GRAPHQL_URL, { - headers: { - Authorization: `Bearer ${process.env.QBO_ACCESS_TOKEN}`, - 'intuit_tid': crypto.randomUUID(), // unique per request - // QBO uses lowercase `realmId` for GraphQL; the official Intuit Node sample - // also tries `intuit-realm-id` — pass `realmId` to be safe. - realmId: process.env.QBO_REALM_ID, - }, -}); - -const QUERY = gql` - query ListDefinitions($filters: AppFoundations_CustomExtensionsDefinitionFilterBy!, $first: Int!, $after: String) { - appFoundationsCustomFieldDefinitions(filters: $filters, first: $first, after: $after) { - edges { node { id legacyIDV2 label dataType active dropDownOptions { id value active order } - associations { associatedEntity associationCondition allowedOperations - subAssociations { associatedEntity active allowedOperations } } } } - pageInfo { hasNextPage endCursor } - } - } -`; - -const data = await client.request(QUERY, { filters: { active: true }, first: 50, after: null }); -``` - -- Send the argument as **`filters`** (plural) with primitive fields: `{ active: true }`. Do not use `{ equals: ... }` predicate wrappers. Filter by target entity client-side after the response — see the main prompt's Task 1 for the `associations[].associatedEntity` + `subAssociations[].associatedEntity` filtering logic. -- Build a `Map` (or plain object) keyed by `legacyIDV2` (NOT the GraphQL `id`). Store `label`, `dataType`, and `dropDownOptions` per entry. The GraphQL node field is lowercase `id`. -- Handle pagination via `pageInfo.endCursor` / `pageInfo.hasNextPage` — loop until `hasNextPage` is `false`. - ---- - -## Task 2 — REST V3 entity creation (`axios` + plain JSON) - -QBO REST V3 expects PascalCase keys on the request body. Build a plain JS object with PascalCase keys and POST via axios. **No SDK is involved.** - -```js -import axios from 'axios'; - -const BASE_REST = process.env.QBO_ENV === 'sandbox' - ? 'https://sandbox-quickbooks.api.intuit.com' - : 'https://quickbooks.api.intuit.com'; - -const body = { - Line: [{ - Amount: 111, - DetailType: 'SalesItemLineDetail', - SalesItemLineDetail: { ItemRef: { value: '1' } }, - }], - CustomerRef: { value: '1' }, - CustomField: [{ - DefinitionId: definition.legacyIDV2, // NOT the GraphQL `id` - Name: definition.label, // optional but recommended - Type: 'StringType', // see "Typed value fields" below - StringValue: 'Demo value', - }], -}; - -const resp = await axios.post( - `${BASE_REST}/v3/company/${process.env.QBO_REALM_ID}/salesreceipt`, - body, - { - params: { - minorversion: '{{minorversion}}', - include: 'enhancedAllCustomFields', // critical — without this, the response strips CustomField metadata - }, - headers: { - Authorization: `Bearer ${process.env.QBO_ACCESS_TOKEN}`, - 'Content-Type': 'application/json', - Accept: 'application/json', - intuit_tid: crypto.randomUUID(), - }, - timeout: 30_000, - } -); - -const created = resp.data.SalesReceipt; // QBO wraps the response in the entity name -console.log('Created SalesReceipt id', created.Id); -``` - -### Typed value fields - -Pick exactly **one** value field per `CustomField` based on Task 1's `dataType`: - -| dataType | `Type` | Value field | Format | -|---|---|---|---| -| `STRING` / `STRING_LIST` / `OBJECT_LIST` | `"StringType"` | `StringValue` | string | -| `NUMBER` | `"NumberType"` | `NumberValue` | number | -| `DATE` | `"DateType"` | `DateValue` | `"YYYY-MM-DD"` | - -The published enum does not list `BOOLEAN`; if the live schema returns one, follow the schema. Skip `UNKNOWN` with a logged warning. Do not hardcode `StringValue` for every entry. - -For `STRING_LIST` / `OBJECT_LIST`, validate `StringValue` against the definition's active `dropDownOptions[].value` set (from Task 1) before sending. - -### Silent-drop detection - -REST V3 silently drops `CustomField` entries whose `DefinitionId` doesn't match the target entity's sub-association. After the POST returns, compare `created.CustomField` against the array you sent and warn on any missing entries. Most common cause: a `SALE_INVOICE`-scoped definition will not attach to a SalesReceipt (`SALE`-scoped) — and vice versa. - ---- - -## Task 3 — Type-aware hydration (`axios` + JSON) - -```js -const resp = await axios.get( - `${BASE_REST}/v3/company/${process.env.QBO_REALM_ID}/salesreceipt/${createdId}`, - { - params: { minorversion: '{{minorversion}}', include: 'enhancedAllCustomFields' }, - headers: { - Authorization: `Bearer ${process.env.QBO_ACCESS_TOKEN}`, - Accept: 'application/json', - intuit_tid: crypto.randomUUID(), - }, - } -); -const fetched = resp.data.SalesReceipt; - -for (const rcf of fetched.CustomField ?? []) { - const meta = defMap.get(rcf.DefinitionId); // DefinitionId equals legacyIDV2 - const value = - meta.dataType === 'NUMBER' ? rcf.NumberValue : - meta.dataType === 'DATE' ? rcf.DateValue : - rcf.StringValue; - // ...render label + value for the UI... -} -``` - -Filter `fetched.Line` to entries where `line.DetailType === 'SalesItemLineDetail'`. Exclude `SubTotalLineDetail`, `DiscountLineDetail`, `TaxLineDetail`. - ---- - -## Required runtime essentials - -- **`dotenv`** — `npm install dotenv`. Call `dotenv.config()` once at startup to load `.env`. Works regardless of cwd as long as `.env` is at the project root. -- **`intuit_tid` per request.** Generate via `crypto.randomUUID()` (Node 18+) and set on every outbound request header. Capture the response `intuit_tid` (`resp.headers['intuit_tid']`) and log it for support correlation. -- **Logging.** Use Node's built-in `console` or a library like `pino` / `winston`. No binding-required setup needed (unlike Java's SLF4J trap). - ---- - -## TypeScript typing (optional) - -If using TypeScript, declare: - -```ts -interface DefinitionMeta { - legacyIDV2: string; - label: string; - dataType: 'STRING' | 'STRING_LIST' | 'OBJECT_LIST' | 'NUMBER' | 'DATE' | 'UNKNOWN'; - dropDownOptions?: { id: string; value: string; active: boolean; order: number }[]; -} - -interface CustomField { - DefinitionId: string; - Name?: string; - Type: 'StringType' | 'NumberType' | 'DateType'; - StringValue?: string; - NumberValue?: number; - DateValue?: string; // YYYY-MM-DD -} -``` diff --git a/discover/custom-fields/sdk-notes/php.md b/discover/custom-fields/sdk-notes/php.md deleted file mode 100644 index bcf217f..0000000 --- a/discover/custom-fields/sdk-notes/php.md +++ /dev/null @@ -1,36 +0,0 @@ -**If generating PHP code (language: `{{language_framework}}`):** - -Use **`GuzzleHttp\Client`** (or PHP's built-in `curl`) for Task 1 (GraphQL discovery — no SDK supports GraphQL) and the official **`quickbooks/v3-php-sdk`** for Tasks 2 & 3 (REST V3). - -- **Task 1 — GraphQL discovery (plain HTTP):** - - The PHP SDK does **not** support GraphQL. Use `GuzzleHttp\Client` (or `curl`) to POST the `appFoundationsCustomFieldDefinitions` query to `{{graphql_endpoint_production}}` (or `{{graphql_endpoint_sandbox}}` when `QBO_ENV=sandbox`). - - Send the argument as **`filters`** (plural — `AppFoundations_CustomExtensionsDefinitionFilterBy` input type) with primitive fields: `{ active: true }`. Do not use `{ equals: ... }` predicate wrappers. Filter by target entity client-side after the response. - - Parse the JSON response and build an associative array keyed by `legacyIDV2` (NOT the GraphQL `id`). Store `label` and `dataType` per entry. The GraphQL node field is lowercase `id`. - - Handle pagination via `pageInfo.endCursor` / `pageInfo.hasNextPage`. - -- **Task 2 — REST V3 entity creation (SDK):** - - `QuickBooksOnline\API\DataService\DataService::Add()` — create the transaction or `Customer`/`Vendor` - - Entity class matching `{{type_of_transaction}}` (e.g., `IPPSalesReceipt`, `IPPInvoice`, `IPPCustomer`, `IPPVendor`) — the entity object. All inherit `$CustomField` (typed `IPPCustomField[]`) via `IPPIntuitEntity`. - - `IPPCustomField` — one instance per custom field. Verified against `intuit/QuickBooks-V3-PHP-SDK` (commit 98223c9, PR #572): - - `$DefinitionId` — pass the **`legacyIDV2`** from Task 1, **not** the GraphQL `id` - - `$Name` — the `label` from Task 1 (recommended for readability) - - `$Type` — map Task 1's `dataType`: `STRING` / `STRING_LIST` / `OBJECT_LIST` → `"StringType"`, `NUMBER` → `"NumberType"`, `DATE` → `"DateType"`. Skip `UNKNOWN` with a logged warning. - - Set exactly ONE typed value property based on `$Type`: `$StringValue` (string), `$NumberValue` (numeric), `$DateValue` (`YYYY-MM-DD`), or `$BooleanValue` (rarely surfaced). **There is no `AnyIntuitObject` property on `IPPCustomField`** — do not assign one. - - Attach the `IPPCustomField` array to the entity's `$CustomField` property before calling `->Add()`. - - **Attach the include parameter via the SDK's public API** — two equivalent options: - - Service-wide: `$dataService->setIncludeParam([\QuickBooksOnline\API\Core\CoreConstants::INCLUDE_ENHANCED_ALL_CUSTOM_FIELDS]);` once on setup, applies to every subsequent call. - - Per-call: `$dataService->Add($entity, [\QuickBooksOnline\API\Core\CoreConstants::INCLUDE_ENHANCED_ALL_CUSTOM_FIELDS])`. `Update()`, `FindById($entity, $id, $includeParam)`, and `Retrieve($entity, $includeParam)` all accept the same trailing parameter. - - Either form appends `?include=enhancedAllCustomFields` (or `&include=...`) to the URL the SDK posts. No plain-Guzzle fallback is required. - -- **Task 3 — Type-aware hydration (SDK):** - - Reuse the same `$dataService` (with `setIncludeParam` already applied) or pass `$includeParam` per-call on `$dataService->FindById($entity, $id, [...])`. - - Iterate the returned entity's `$CustomField` array. The REST response's `$DefinitionId` equals the `legacyIDV2` you stored in Task 1 — use it to look up `label` and `dataType` from your cached map. - - Use the cached `dataType` to read the matching typed property: `$rcf->StringValue`, `$rcf->NumberValue`, `$rcf->DateValue`. (No `AnyIntuitObject` accessor.) - -- **Composer install:** - ```bash - composer require quickbooks/v3-php-sdk - ``` -- **SDK reference:** `{{php-sdk-documentation}}` - -> Warning: Use **only** methods and classes that exist in the published SDK. Do not construct fake SDK models or invent method signatures. diff --git a/discover/custom-fields/sdk-notes/python.md b/discover/custom-fields/sdk-notes/python.md deleted file mode 100644 index 1728580..0000000 --- a/discover/custom-fields/sdk-notes/python.md +++ /dev/null @@ -1,177 +0,0 @@ -**If generating Python code (language: `{{language_framework}}`):** - -There is no official Intuit-published Python SDK, but the community-maintained **`python-quickbooks`** library (by `ej2`, v0.9.12+) is the de-facto standard. It has typed entity models, custom-field wiring on Invoice/SalesReceipt, and a public `params={}` hook on `.save()` / `.get()` that propagates to the QBO REST URL — so `params={'include': 'enhancedAllCustomFields'}` does the right thing. - -For Task 1 (GraphQL), use `requests` directly — no library supports the Custom Fields GraphQL API in Python. - ---- - -## Task 1 — GraphQL discovery (`requests`) - -- POST the `appFoundationsCustomFieldDefinitions` query to `{{graphql_endpoint_production}}` (production only — GraphQL has no sandbox endpoint) with `Content-Type: application/json`, `Authorization: Bearer `, and a unique `intuit_tid` per request for log correlation. -- Send the argument as **`filters`** (plural — `AppFoundations_CustomExtensionsDefinitionFilterBy` input type) with primitive fields: `{ active: true }`. Do not use `{ equals: ... }` predicate wrappers. Filter by target entity client-side after the response. -- Parse the JSON response and build a `dict` keyed by `legacyIDV2` (NOT the GraphQL `id`). Store `label` and `dataType` per entry. The GraphQL node field is lowercase `id`. -- Handle pagination via `pageInfo.endCursor` / `pageInfo.hasNextPage`. - ---- - -## Task 2 — REST V3 entity creation (`python-quickbooks`) - -### Setup - -```python -from intuitlib.client import AuthClient -from quickbooks import QuickBooks -from quickbooks.objects.invoice import Invoice -from quickbooks.objects.salesreceipt import SalesReceipt -from quickbooks.objects.base import CustomField, Ref -from quickbooks.objects.detailline import SalesItemLineDetail, SalesItemLine - -auth_client = AuthClient( - client_id=..., - client_secret=..., - environment='production', # or 'sandbox' — honors {{type_of_transaction}}-relevant REST V3 envs only - redirect_uri=..., -) -auth_client.access_token = access_token # from .env - -client = QuickBooks( - auth_client=auth_client, - refresh_token=refresh_token, # optional, only if you'll refresh - company_id=realm_id, - minorversion={{minorversion}}, -) -``` - -### Build the entity - -```python -sr = SalesReceipt() -sr.CustomerRef = Ref() -sr.CustomerRef.value = "1" - -line = SalesItemLine() -line.Amount = 111 -line.SalesItemLineDetail = SalesItemLineDetail() -line.SalesItemLineDetail.ItemRef = Ref() -line.SalesItemLineDetail.ItemRef.value = "1" -sr.Line.append(line) - -cf = CustomField() -cf.DefinitionId = definition["legacyIDV2"] # NOT the GraphQL `id` -cf.Name = definition["label"] # optional but recommended -cf.Type = "StringType" # pick from StringType / NumberType / DateType -cf.StringValue = "Demo value" # see "Typed value fields" below -sr.CustomField.append(cf) - -# Attach the include parameter via `params=` on .save(). -# This is the key call — without it, the response strips CustomField metadata. -sr.save(qb=client, params={'include': 'enhancedAllCustomFields'}) - -print("Created SalesReceipt id =", sr.Id) -``` - -### Typed value fields — important Python-only gap - -The `quickbooks.objects.base.CustomField` class in `python-quickbooks` 0.9.12 only declares **`StringValue`** as a typed attribute. **`NumberValue` and `DateValue` are NOT typed on the class** — to set them, assign them as regular attributes and they'll serialize correctly via `to_json()`: - -```python -# STRING / STRING_LIST / OBJECT_LIST — uses the typed StringValue field -cf.Type = "StringType" -cf.StringValue = "Demo value" - -# NUMBER — set NumberValue as a raw attribute (not part of the typed POJO) -cf.Type = "NumberType" -cf.NumberValue = 42.0 - -# DATE — set DateValue as a raw attribute, format YYYY-MM-DD -cf.Type = "DateType" -cf.DateValue = "2026-05-23" -``` - -Set **exactly one** value field per `CustomField`. For `STRING_LIST` / `OBJECT_LIST`, validate `StringValue` against the definition's active `dropDownOptions[].value` set (from Task 1) before sending. - -### Customer entity — Python-only gap - -`python-quickbooks` 0.9.12's `Customer` class does **NOT** declare a `CustomField` attribute, even though the QBO REST API supports custom fields on Customer. If you need Customer custom fields, you have three options: -1. Monkey-patch: `Customer.CustomField = []` and add it to `Customer.list_dict` before serialization. -2. Use the SDK-free path below for Customer specifically. -3. Submit a PR upstream. - -### Silent-drop detection - -REST V3 silently drops `CustomField` entries whose `DefinitionId` doesn't match the target entity's sub-association. After `.save()` returns (the SDK populates `self.CustomField` from the response), compare the returned `CustomField` list against the list you sent. Warn on any missing entries — most common cause: a `SALE_INVOICE`-scoped definition will not attach to a SalesReceipt (`SALE`-scoped) — and vice versa. - ---- - -## Task 3 — Type-aware hydration (`python-quickbooks`) - -```python -fetched = SalesReceipt.get(sr.Id, qb=client, params={'include': 'enhancedAllCustomFields'}) - -for rcf in fetched.CustomField: - def_id = rcf.DefinitionId # equals legacyIDV2 - meta = definition_map[def_id] - data_type = meta['dataType'] - if data_type in ('STRING', 'STRING_LIST', 'OBJECT_LIST'): - value = rcf.StringValue - elif data_type == 'NUMBER': - value = getattr(rcf, 'NumberValue', None) - elif data_type == 'DATE': - value = getattr(rcf, 'DateValue', None) - # ...render label + value for the UI... -``` - -Filter `fetched.Line` to entries where `line.DetailType == "SalesItemLineDetail"`. Exclude system-generated lines (`SubTotalLineDetail`, `DiscountLineDetail`, `TaxLineDetail`). - ---- - -## Dependencies - -``` -pip install python-quickbooks intuit-oauth -``` - -`intuit-oauth` provides `AuthClient` (the OAuth flow). Use it only if you need refresh; this prompt assumes the token in `.env` is already valid. - ---- - -## Alternative: no-SDK approach (`requests` + dicts) - -Use this when you don't want to add `python-quickbooks` to the project, or when you need Customer custom fields (which the SDK doesn't wire — see above). The pattern matches what the Node.js notes do: build the request body as a `dict` with PascalCase keys, POST via `requests.post(...)` with `params={'include': 'enhancedAllCustomFields'}`. - -```python -import requests, uuid - -body = { - "Line": [{ - "Amount": 111, - "DetailType": "SalesItemLineDetail", - "SalesItemLineDetail": {"ItemRef": {"value": "1"}}, - }], - "CustomerRef": {"value": "1"}, - "CustomField": [{ - "DefinitionId": definition["legacyIDV2"], - "Name": definition["label"], - "Type": "StringType", - "StringValue": "Demo value", - }], -} - -resp = requests.post( - f"https://quickbooks.api.intuit.com/v3/company/{realm_id}/{{type_of_transaction}}", - params={"minorversion": "{{minorversion}}", "include": "enhancedAllCustomFields"}, - headers={ - "Authorization": f"Bearer {access_token}", - "Content-Type": "application/json", - "Accept": "application/json", - "intuit_tid": str(uuid.uuid4()), - }, - json=body, - timeout=30, -) -resp.raise_for_status() -created = resp.json()["SalesReceipt"] # QBO wraps the response in the entity name -``` - -PascalCase keys, top-level entity for request, response wrapped under PascalCase entity name. Same shape on the wire as the SDK path. diff --git a/discover/custom-fields/sdk-notes/ruby.md b/discover/custom-fields/sdk-notes/ruby.md deleted file mode 100644 index b9c5d70..0000000 --- a/discover/custom-fields/sdk-notes/ruby.md +++ /dev/null @@ -1,157 +0,0 @@ -**If generating Ruby code (language: `{{language_framework}}`):** - -Use the community-maintained **`quickbooks-ruby`** gem for Tasks 2 and 3, and Ruby's built-in **`Net::HTTP`** (or the **`faraday`** gem) for Task 1 (no SDK supports GraphQL). `quickbooks-ruby` has typed `CustomField` with all four value types, full wiring on `Invoice` and `SalesReceipt`, and a public `params:` argument on `fetch_by_id` / `query:` on `create` that propagates to the REST URL — so `params: { include: 'enhancedAllCustomFields' }` does the right thing. - ---- - -## Task 1 — GraphQL discovery (`Net::HTTP` / `faraday`) - -- POST the `appFoundationsCustomFieldDefinitions` query to `{{graphql_endpoint_production}}` (production only — GraphQL has no sandbox endpoint) with `Content-Type: application/json`, `Authorization: Bearer `, and a unique `intuit_tid` per request. -- Send the argument as **`filters`** (plural — `AppFoundations_CustomExtensionsDefinitionFilterBy` input type) with primitive fields: `{ active: true }`. Do not use `{ equals: ... }` predicate wrappers. Filter by target entity client-side after the response. -- Parse the JSON response and build a `Hash` keyed by `legacyIDV2` (NOT the GraphQL `id`). Store `label` and `dataType` per entry. The GraphQL node field is lowercase `id`. -- Handle pagination via `pageInfo.endCursor` / `pageInfo.hasNextPage`. - ---- - -## Task 2 — REST V3 entity creation (`quickbooks-ruby`) - -### Setup - -```ruby -require 'quickbooks-ruby' - -Quickbooks.minorversion = {{minorversion}} - -oauth_client = OAuth2::Client.new(client_id, client_secret, site: 'https://oauth.platform.intuit.com') -access_token = OAuth2::AccessToken.new(oauth_client, env['QBO_ACCESS_TOKEN']) - -service = Quickbooks::Service::SalesReceipt.new -service.company_id = realm_id -service.access_token = access_token -``` - -### Build the entity - -```ruby -sr = Quickbooks::Model::SalesReceipt.new -sr.customer_id = "1" - -line = Quickbooks::Model::SalesReceiptLineItem.new -line.amount = 111 -line.sales_item! do |sales_item| - sales_item.item_id = "1" -end -sr.line_items << line - -cf = Quickbooks::Model::CustomField.new -cf.id = definition['legacyIDV2'].to_i # legacyIDV2 from Task 1 (NOT the GraphQL id). - # quickbooks-ruby maps `DefinitionId` → `id` on the model. -cf.name = definition['label'] # optional but recommended -cf.type = "StringType" # pick StringType / NumberType / DateType / BooleanType based on dataType -cf.string_value = "Demo value" # see "Typed value fields" below -sr.custom_fields << cf - -# Attach the include parameter via `query:` in the `create` options. -created = service.create(sr, query: { include: 'enhancedAllCustomFields' }) - -puts "Created SalesReceipt id = #{created.id}" -``` - -### Typed value fields - -`Quickbooks::Model::CustomField` (in `lib/quickbooks/model/custom_field.rb`) exposes all four typed accessors. Set **exactly one** based on `dataType`: - -- `STRING` / `STRING_LIST` / `OBJECT_LIST` → `cf.string_value = "..."` -- `NUMBER` → `cf.number_value = 42` -- `DATE` → `cf.date_value = Date.parse("2026-05-23")` (a Ruby `Date`) -- `BooleanType` → `cf.boolean_value = true` (rarely surfaced) - -There is also a polymorphic `cf.value = ...` setter that picks the right typed field based on `cf.type`, but for clarity prefer the explicit typed accessor. - -For `STRING_LIST` / `OBJECT_LIST`, validate `string_value` against the definition's active `dropDownOptions[].value` set (from Task 1) before sending. - -### Customer entity — Ruby-only gap - -`quickbooks-ruby`'s `Customer` model does **NOT** wire `CustomField`, even though the QBO REST API supports it on Customer. If you need Customer custom fields: -1. Monkey-patch: `Quickbooks::Model::Customer.xml_accessor :custom_fields, from: 'CustomField', as: [Quickbooks::Model::CustomField]` -2. Or use the SDK-free path below for Customer specifically. - -### Silent-drop detection - -REST V3 silently drops `CustomField` entries whose `DefinitionId` doesn't match the target entity's sub-association. After `service.create(...)` returns, compare `created.custom_fields` against the list you sent. Warn on any missing entries. - ---- - -## Task 3 — Type-aware hydration (`quickbooks-ruby`) - -```ruby -fetched = service.fetch_by_id(created.id, include: 'enhancedAllCustomFields') - -fetched.custom_fields.each do |rcf| - def_id = rcf.id.to_s # equals legacyIDV2 - meta = definition_map[def_id] - value = case meta[:dataType] - when 'STRING', 'STRING_LIST', 'OBJECT_LIST' then rcf.string_value - when 'NUMBER' then rcf.number_value - when 'DATE' then rcf.date_value - end - # ...render label + value for the UI... -end -``` - -Filter `fetched.line_items` to entries where the underlying detail type is `SalesItemLineDetail`. The gem exposes detail-type checks via `line.sales_item?`, `line.subtotal?`, etc. — use those. - ---- - -## Dependencies - -```ruby -# Gemfile -gem 'quickbooks-ruby' -``` - -```bash -bundle install -``` - ---- - -## Alternative: no-SDK approach (`Net::HTTP` / `faraday` + Hash) - -Use this when you don't want the `quickbooks-ruby` dependency, or when you need Customer custom fields (which the SDK doesn't wire). Build the request body as a `Hash` with PascalCase keys, POST via `Net::HTTP` or `Faraday` with `include=enhancedAllCustomFields` on the URL. - -```ruby -require 'net/http' -require 'json' -require 'securerandom' - -body = { - Line: [{ - Amount: 111, - DetailType: "SalesItemLineDetail", - SalesItemLineDetail: { ItemRef: { value: "1" } } - }], - CustomerRef: { value: "1" }, - CustomField: [{ - DefinitionId: definition[:legacyIDV2], - Name: definition[:label], - Type: "StringType", - StringValue: "Demo value" - }] -} - -uri = URI("https://quickbooks.api.intuit.com/v3/company/#{realm_id}/{{type_of_transaction}}?minorversion={{minorversion}}&include=enhancedAllCustomFields") -req = Net::HTTP::Post.new(uri) -req['Authorization'] = "Bearer #{access_token}" -req['Content-Type'] = 'application/json' -req['Accept'] = 'application/json' -req['intuit_tid'] = SecureRandom.uuid -req.body = body.to_json - -resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) } -raise "QBO #{resp.code}: #{resp.body}" unless resp.code.to_i.between?(200, 299) - -created = JSON.parse(resp.body)['SalesReceipt'] # QBO wraps the response in the entity name -``` - -PascalCase keys for the request, response wrapped under PascalCase entity name. Same shape on the wire as the SDK path. diff --git a/discover/dimensions/prompt-template-dimensions.md b/discover/dimensions/prompt-template-dimensions.md index 083203e..e95f54b 100644 --- a/discover/dimensions/prompt-template-dimensions.md +++ b/discover/dimensions/prompt-template-dimensions.md @@ -1,6 +1,12 @@ **Role:** You are a Principal Software Engineer specializing in Intuit Enterprise Suite (IES) integrations. -**Context:** I am developing a `{{language_framework}}` application. I need to implement a workflow that uses the IES Dimensions APIs to tag `{{type_of_transaction}}`. Assume the application already has a valid OAuth 2.0 access token and IES realmId (Company ID) and environment (production or sandbox) available in the .env file. Focus strictly on the API integration logic. +**Context:** I am developing a `{{language_framework}}` application. I need to implement a workflow that uses the IES Dimensions APIs to tag `{{type_of_transaction}}`. Assume the application already has a valid OAuth 2.0 access token and IES realmId (Company ID) and environment (production or sandbox) available in the `.env` file. Focus strictly on the API integration logic. + +**References:** +- Dimensions documentation: `{{dimension_Api_documentation}}` +- GraphQL schema reference: `{{graphql_schema}}` +- REST V3 API documentation: `{{rest_v3_api_documentation}}` +- OAuth 2.0 documentation: `{{oauth2-documentation}}` --- @@ -52,7 +58,7 @@ Ensure the Line items include the `CustomExtensions` array exactly as follows: ``` **Constraints:** -- Use `minorversion=75` to ensure `CustomExtensions` are processed. +- Use `minorversion={{minorversion}}` to ensure `CustomExtensions` are processed. - Each transaction line can have a maximum of one `AssociatedValues.value` per dimension. --- @@ -86,7 +92,7 @@ Once the transaction is created, I need to display it to the user. 1. **No Hallucinations:** Do not invent, guess, or hallucinate API endpoints, GraphQL properties, or SDK methods that are not explicitly provided in the context or linked documentation. 2. **Strict SDK/Library Usage:** If an official SDK or library is specified (e.g., Intuit Java SDK), use ONLY the methods and classes that exist in its latest public release. Do not construct fake SDK models. 3. **Provided Links Only:** You must derive all API syntax, structure, and constraints strictly from the provided links. All HTTP responses (GraphQL and REST) must be parsed according to the provided documentation. -4. **Endpoint Strictness:** Use the exact endpoints and query structures provided. Do not attempt to modify the base URL, append unsupported parameters, or alter the `minorversion=75` requirement. +4. **Endpoint Strictness:** Use the exact endpoints and query structures provided. Do not attempt to modify the base URL, append unsupported parameters, or alter the `minorversion={{minorversion}}` requirement. 5. **If Blocked/Missing Info:** If the provided documentation or payload structures lack required fields to compile a functional request, STOP and clearly state what specific information is missing instead of making an educated guess. I have provided you with all the necessary context and instructions. Please generate the code and documentation as per the instructions. \ No newline at end of file diff --git a/discover/generated-prompts/custom-fields-ready-prompt.md b/discover/generated-prompts/custom-fields-ready-prompt.md new file mode 100644 index 0000000..a90ab73 --- /dev/null +++ b/discover/generated-prompts/custom-fields-ready-prompt.md @@ -0,0 +1,268 @@ +**Role:** You are a Principal Software Engineer specializing in QuickBooks Online integrations. + +**Context:** I am developing a `python` application using `hints (dataclasses)` typing. I need to implement a workflow that uses the QuickBooks Custom Fields APIs to attach custom metadata to `salesreceipt` transactions (and/or to `Customer` / `Vendor` / `Project` entities, depending on QBO tier). Assume the application already has a valid OAuth 2.0 access token, realmId (Company ID), and environment (production or sandbox) available in the `.env` file. Note: the Custom Fields GraphQL API is production-only — Tasks 1, 4, and 5 always target production regardless of `QBO_ENV`. Tasks 2 and 3 (REST V3) honor `QBO_ENV`. Focus strictly on the API integration logic. + +**References:** +- Custom Fields documentation: `https://developer.intuit.com/app/developer/qbo/docs/workflows/create-custom-fields` +- GraphQL schema reference: `https://developer.intuit.com/app/developer/gql/docs/api/qbexternal/queries/` +- OAuth 2.0 documentation: `https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization/oauth-2.0` +- Official sample apps: `https://github.com/IntuitDeveloper/SampleApp-CustomFields-Java` · `https://github.com/IntuitDeveloper/SampleApp-CustomFields-Python` + +--- + +## Task 1: Discover Custom Field Definitions (GraphQL) + +**Capability Check:** Custom Fields availability and limits vary by QBO tier (Essentials, Plus, Advanced/IES) and by target entity. Entity-level custom fields on `Customer`/`Vendor`/`Project` require Advanced or IES. If any call returns 403 with "Feature Not Enabled" or a tier-insufficient error, surface a user-friendly message: *"Custom Fields are not available for this account's tier or entity. Configure them in QuickBooks settings, or upgrade the account."* + +Fetch all active definitions, then filter client-side by `associations[].associatedEntity` to find the ones relevant to your target entity. + +- **Endpoint:** `https://qb.api.intuit.com/graphql` (production-only — no sandbox). +- **Headers:** `Authorization: Bearer `, `Content-Type: application/json`, `realmId: `, and a unique `intuit_tid` per request for log correlation. +- **Query:** `query GetCustomFieldDefinitions($cursor: String) { + appFoundationsCustomFieldDefinitions( + filters: { active: true } + first: 50 + after: $cursor + ) { + edges { + node { + id + legacyID + legacyIDV2 + label + dataType + required + active + colorCode + entityVersion + createdSource + dropDownOptions { id value active order } + associations { + associatedEntity + associationCondition + active + allowedOperations + subAssociations { associatedEntity active allowedOperations } + } + } + } + pageInfo { hasNextPage hasPreviousPage startCursor endCursor } + } +}` +- **Variables:** `{ "cursor": null }` on the first call; pass `{ "cursor": "" }` while `pageInfo.hasNextPage` is `true`. + +### Extract per definition node + +- `id` — the GraphQL mutation handle (returns as `udcf_`). Required by the Task 5 update mutation. +- `legacyIDV2` — the value to pass as `DefinitionId` in every REST V3 call. **Critical bridge from GraphQL → REST.** Also required (alongside `id` and `label`) by Task 5. +- `legacyID` — deprecated opaque form. Skip; use `legacyIDV2` everywhere. +- `label`, `dataType`, `required`, `active`, `colorCode`, `entityVersion`, `createdSource` — definition metadata. +- `dropDownOptions[]` — populated for `STRING_LIST` / `OBJECT_LIST` types (empty array `[]` otherwise). Each option: `{ id, value, active, order }`. Validate list-type writes against this set. +- `associations[]` — which entities the definition is scoped to: + - `associatedEntity` — parent path, one of `"/transactions/Transaction"`, `"/network/Contact"`, `"/work/Project"`. + - `associationCondition` — `INCLUDED` or `EXCLUDED`. Treat missing as `INCLUDED`. + - `allowedOperations` — subset of `SEARCH`, `PRINT`, `REPORTS`. Empty list means the field doesn't surface in those UI contexts. + - `subAssociations[]` — narrows to specific sub-types. **Field is `associatedEntity` (same name as parent, not `associatedSubEntity`)**, with `UPPER_SNAKE_CASE` values. Known valid codes under `/transactions/Transaction`: `SALE` (SalesReceipt), `SALE_INVOICE` (Invoice), `SALE_ESTIMATE` (Estimate), `SALE_CREDIT` (CreditMemo), `SALE_REFUND` (RefundReceipt). Known valid codes under `/network/Contact`: `CUSTOMER`, `VENDOR`. Known valid code under `/work/Project`: `PROJECT`. **Matching is exact** — a sub-association of `SALE` matches SalesReceipt only; it does NOT match Invoices. Use `SALE_INVOICE` for Invoices. + +Build an in-memory map keyed by `legacyIDV2` storing `label`, `dataType`, `dropDownOptions`, and `associations`. Handle pagination until `pageInfo.hasNextPage` is `false`. + +**Empty State:** If no definitions match the target entity after client-side filtering, surface: *"No active Custom Field definitions found for `salesreceipt`. Configure them in QuickBooks settings, or run Task 4 (create)."* + +--- + +## Task 2: Attach Custom Field Values to a Transaction or Entity (REST V3) + +Create one salesreceipt with default item id: 1, default customer: 1, and amount: 111. Attach at least one custom field value using a DefinitionId discovered in Task 1. + +### Data flow for the CustomField array + +- `DefinitionId` = `legacyIDV2` from Task 1 (NOT the GraphQL `id`). +- `Name` = `label` from Task 1 (optional, recommended). +- `Type` and value field derived from Task 1's `dataType`: + +| `dataType` | `Type` | Value field | Format | +|---|---|---|---| +| `STRING` | `StringType` | `StringValue` | string | +| `NUMBER` | `NumberType` | `NumberValue` | decimal | +| `DATE` | `DateType` | `DateValue` | `YYYY-MM-DD` | +| `STRING_LIST` / `OBJECT_LIST` | `StringType` | `StringValue` | must match a `value` from `dropDownOptions[]` where `active: true` — validate before sending | +| `UNKNOWN` | (skip) | (skip) | log warning, do not write | + +Set exactly one value field per `CustomField` entry. + +### API Details + +- **Endpoint:** `POST /v3/company/{{companyid}}/salesreceipt?minorversion=75` +- **Required query parameter:** Append `include=enhancedAllCustomFields` to every create/read URL — without it, the response will not include custom-field metadata. +- **Documentation:** `https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/salesreceipt` and `https://developer.intuit.com/app/developer/qbo/docs/workflows/create-custom-fields` + +### Payload structure + +```json +[ + { + "DefinitionId": "{{DefinitionId}}", + "Name": "{{FieldName}}", + "Type": "StringType", + "StringValue": "{{FieldValue}}" + } +] +``` + +**Constraints:** +- Use `minorversion=75` on all REST V3 calls. +- `DefinitionId` must be a `legacyIDV2` from Task 1 — do not invent or hardcode IDs. +- REST V3 **silently drops** `CustomField` entries whose `DefinitionId` doesn't match the target entity's sub-association exactly (e.g. a definition scoped to `SALE` won't attach to an Invoice — only `SALE_INVOICE` will). HTTP 200 returns with the dropped entries omitted and no error. **Compare the response's `CustomField` array against the request and warn on any drops.** + +--- + +## Task 3: Read & Hydrate for UI + +Retrieve the entity and display custom field values with human-readable labels. + +- **Fetch:** Use `GET /v3/company/{{companyid}}/salesreceipt/{{TransactionId}}` and append `include=enhancedAllCustomFields`. +- **Hydration:** For each `CustomField` in the response, look up `DefinitionId` (= `legacyIDV2`) in the Task 1 map to get `label` and `dataType`. +- **Type-aware reading:** Use the cached `dataType` to read the matching value field (`StringValue` / `NumberValue` / `DateValue`). Parse `DateValue` as `YYYY-MM-DD`. +- **Display:** Show the entity ID, date, total amount, and a labelled list of custom fields and their values. +- **Line Filtering:** Only hydrate `SalesItemLineDetail` lines. Exclude system-generated lines (`SubTotalLineDetail`, `DiscountLineDetail`, `TaxLineDetail`). + +--- + +## Task 4 (optional): Create a New Custom Field Definition (GraphQL) + +Use only when your app provisions custom fields as part of onboarding. Requires the `app-foundations.custom-field-definitions` scope. + +- **Mutation:** `mutation CreateCustomFieldDefinition($input: AppFoundations_CustomFieldDefinitionCreateInput!) { + appFoundationsCreateCustomFieldDefinition(input: $input) { + id + legacyIDV2 + label + dataType + required + active + dropDownOptions { id value active order } + associations { + associatedEntity + associationCondition + active + allowedOperations + subAssociations { associatedEntity active allowedOperations } + } + } +}` +- **Variables:** +```json +{ + "input": { + "active": true, + "label": "Project Code", + "dataType": "STRING", + "associations": [ + { + "active": true, + "associatedEntity": "/transactions/Transaction", + "associationCondition": "INCLUDED", + "allowedOperations": [], + "subAssociations": [ + { "active": true, "associatedEntity": "SALE_INVOICE", "allowedOperations": [] } + ] + } + ] + } +} +``` + +### Input field reference (`AppFoundations_CustomFieldDefinitionCreateInput`) + +- `label` (String, required) — display name, **max 30 characters**. Exceeding returns `LABEL_LENGTH_EXCEEDED`. +- `dataType` (enum, required) — `STRING`, `NUMBER`, `DATE`, `STRING_LIST`, `OBJECT_LIST`, or `UNKNOWN`. No `_TYPE` suffix and no `BOOLEAN_TYPE`. +- `active` (Boolean) — `true` on create. Use Task 5 with `active: false` to disable later. +- `required` (Boolean) — when `true`, REST V3 rejects writes that omit the field. +- `associations[]` (required) — list of `{ active, associatedEntity, associationCondition, allowedOperations, subAssociations }`. `associatedEntity` is **path-style on writes** (`"/transactions/Transaction"`, `"/network/Contact"`, `"/work/Project"`). `subAssociations[].associatedEntity` uses UPPER_SNAKE values (`SALE_INVOICE`, `CUSTOMER`, etc.). +- `dropDownOptions[]` — required when `dataType` is `STRING_LIST` or `OBJECT_LIST`. Each: `{ value, order }`. + +A single definition can attach to multiple parents (e.g. `/transactions/Transaction` + `/network/Contact`), but combining all three parent paths in one definition returns `MUTUAL_EXCLUSIVITY_IN_SUB_ASSOCIATIONS_VIOLATED` — split into separate definitions. + +--- + +## Task 5 (optional): Update or Disable a Definition (GraphQL) + +The schema exposes **one mutation** for both — there's no separate disable mutation. Disable by calling update with `active: false`. + +- **Mutation:** `mutation UpdateCustomFieldDefinition($input: AppFoundations_CustomFieldDefinitionUpdateInput!) { + appFoundationsUpdateCustomFieldDefinition(input: $input) { + id + legacyIDV2 + label + dataType + active + required + associations { + associatedEntity + associationCondition + active + allowedOperations + } + } +}` +- **Variables (rename):** +```json +{ + "input": { + "id": "", + "legacyIDV2": "", + "label": "Project Code (renamed)" + } +} +``` +- **Variables (disable):** +```json +{ + "input": { + "id": "", + "legacyIDV2": "", + "label": "", + "active": false + } +} +``` + +### Required input fields (service layer enforces all three even though the schema marks two optional) + +- `id` — the `udcf_*` value from Task 1. +- `legacyIDV2` — the bare numeric value from Task 1. Omitting returns `FIELD_MISSING: "You have to provide a value for legacyIDV2"`. +- `label` — the current label from Task 1. Omitting (even on a disable call) returns `"You must specify a value for label."`. + +Optional: `dataType` (risky to change on populated fields), `active`, `associations[]` (full replace, not partial merge), `dropDownOptions[]` (full replace). + +Invalidate the Task 1 cache after a successful update. + +--- + +## Technical Best Practices: + +- **Caching:** Cache the Task 1 definition map for 1 hour. Invalidate after Task 4 or Task 5. +- **Error Handling:** Include blocks for: + - `401 Unauthorized` (XML body from the gateway, not JSON) — refresh the access token via OAuth. + - `403 Forbidden` — missing scope, Custom Fields disabled on the company, or app tier below Silver. + - `400 Bad Request` — log the response body. Common causes: using the GraphQL `id` as `DefinitionId` in REST V3, wrong value field for the `Type`, omitting `include=enhancedAllCustomFields`. + - **HTTP 200 with `errors[]`** — always inspect; a 200 doesn't mean success. Watch for `extensions.errorCode.errorCode` values like `AUTHORIZATION_DENIED` (missing write scope), `LABEL_LENGTH_EXCEEDED`, `MUTUAL_EXCLUSIVITY_IN_SUB_ASSOCIATIONS_VIOLATED`, `FIELD_MISSING`. + - **Silent drops** — after every REST V3 create, compare the request's `CustomField` array against the response. Warn on missing entries. +- **Observability:** Capture and log the `intuit_tid` header on every response. NEVER log access tokens, OAuth secrets, or PII. +- **Typing:** Provide `hints (dataclasses)` models for `CustomFieldDefinition`, `CustomField`, and (if using Task 4) `CustomFieldDefinitionCreateInput`. +- **Output (integration mode: `new`):** Provide modular, clean code and a runnable verification example. + - **If mode is `new`:** Create a self-contained project in a dedicated folder named `custom-fields-python` (no spaces). Include a `README.md` explaining how to run the code, a dependency manifest, and a brief architectural diagram showing the data flow from GraphQL to REST. + - **If mode is `existing`:** Produce modular, well-documented functions/classes/files designed to be imported into an existing codebase. Do **not** scaffold a new project structure. Provide clear integration notes describing which files to add, what imports are needed, and how to wire the functions into an existing app. + +--- + +## 🛑 AI Guardrails (Anti-Hallucination Constraints) + +**CRITICAL INSTRUCTIONS - YOU MUST ADHERE TO THE FOLLOWING:** +1. **No Hallucinations:** Do not invent, guess, or hallucinate API endpoints, GraphQL properties, or SDK methods that are not explicitly provided in the context or linked documentation. +2. **Strict SDK/Library Usage:** If an official SDK or library is specified (e.g., Intuit Java SDK), use ONLY the methods and classes that exist in its latest public release. Do not construct fake SDK models. For Custom Fields specifically: GraphQL (Tasks 1, 4, 5) has no SDK — use a plain HTTP client. For REST V3 (Tasks 2 & 3), the deciding constraint is the mandatory `include=enhancedAllCustomFields` URL parameter: use the SDK's create/read methods (`DataService.add()` / `findById()` in Java, `dataService.Add()` in .NET) **only when the SDK exposes a public hook to attach that parameter**. If it does not — as with the Python and Node.js SDKs — do not force it through the SDK: build the request body as a plain object with PascalCase keys and POST via plain HTTP with `&include=enhancedAllCustomFields` on the URL. +3. **Provided Links Only:** You must derive all API syntax, structure, and constraints strictly from the provided links. All HTTP responses (GraphQL and REST) must be parsed according to the provided documentation. +4. **Endpoint Strictness:** Use the exact endpoints and query structures provided. Do not attempt to modify the base URL, append unsupported parameters, or alter the `minorversion=75` requirement. +5. **If Blocked/Missing Info:** If the provided documentation or payload structures lack required fields to compile a functional request, STOP and clearly state what specific information is missing instead of making an educated guess. + +I have provided you with all the necessary context and instructions. Please generate the code and documentation as per the instructions. diff --git a/discover/generated-prompts/dimensions-ready-prompt.md b/discover/generated-prompts/dimensions-ready-prompt.md index 4b643d5..7fd37ea 100644 --- a/discover/generated-prompts/dimensions-ready-prompt.md +++ b/discover/generated-prompts/dimensions-ready-prompt.md @@ -1,6 +1,12 @@ **Role:** You are a Principal Software Engineer specializing in Intuit Enterprise Suite (IES) integrations. -**Context:** I am developing a `python` application. I need to implement a workflow that uses the IES Dimensions APIs to tag `salesreceipt`. Assume the application already has a valid OAuth 2.0 access token and IES realmId (Company ID) and environment (production or sandbox) available in the .env file. Focus strictly on the API integration logic. +**Context:** I am developing a `python` application. I need to implement a workflow that uses the IES Dimensions APIs to tag `salesreceipt`. Assume the application already has a valid OAuth 2.0 access token and IES realmId (Company ID) and environment (production or sandbox) available in the `.env` file. Focus strictly on the API integration logic. + +**References:** +- Dimensions documentation: `https://developer.intuit.com/app/developer/ies/docs/workflows/manage-dimensions/dimensions-use-cases` +- GraphQL schema reference: `https://developer.intuit.com/app/developer/gql/docs/api/qbexternal/queries/` +- REST V3 API documentation: `https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/salesreceipt` +- OAuth 2.0 documentation: `https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization/oauth-2.0` --- @@ -74,7 +80,7 @@ Create salesreceipt with default item id : 1 and default customer : 1 and salesr ### API Details: - **Endpoint:** `POST /v3/company/{{companyid}}/salesreceipt?minorversion=75` -- **Documentation:** Refer to `https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/account` and `https://developer.intuit.com/app/developer/ies/docs/workflows/manage-dimensions/dimensions-use-cases` +- **Documentation:** Refer to `https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/salesreceipt` and `https://developer.intuit.com/app/developer/ies/docs/workflows/manage-dimensions/dimensions-use-cases` ### Payload Structure: Ensure the Line items include the `CustomExtensions` array exactly as follows: diff --git a/discover/generated-prompts/projects-ready-prompt.md b/discover/generated-prompts/projects-ready-prompt.md index fa6887d..84cf3da 100644 --- a/discover/generated-prompts/projects-ready-prompt.md +++ b/discover/generated-prompts/projects-ready-prompt.md @@ -2,6 +2,12 @@ **Context:** I am developing a `python` application. I need to implement a workflow that uses the Projects APIs to create a project and generate project estimates for IES or QuickBooks Advanced companies. Assume the application already has a valid OAuth 2.0 access token, realmId (Company ID), and environment (production or sandbox) available in the `.env` file. Focus strictly on the API integration logic. +**References:** +- Project Estimate documentation: `https://developer.intuit.com/app/developer/qbo/docs/workflows/manage-projects/use-cases#use-case-5` +- GraphQL schema reference: `https://developer.intuit.com/app/developer/gql/docs/api/qbexternal/queries/` +- REST V3 API documentation: `https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/salesreceipt` +- OAuth 2.0 documentation: `https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization/oauth-2.0` + --- ## Task 1: Pre-flight & Discovery @@ -65,7 +71,7 @@ Create an estimate with default item id : 1 ,UnitPrice: 1, Qty: 100, and ItemAcc ### API Details: - **Endpoint:** `POST /v3/company/{{companyid}}/estimate?minorversion=75` -- **Documentation:** Refer to `https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/account` and `https://developer.intuit.com/app/developer/qbo/docs/workflows/manage-projects/use-cases#use-case-5` +- **Documentation:** Refer to `https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/salesreceipt` and `https://developer.intuit.com/app/developer/qbo/docs/workflows/manage-projects/use-cases#use-case-5` ### Payload Structure: Ensure the Estimate request body includes **both** `ProjectRef` and `CustomerRef` at the top level, exactly as follows: diff --git a/discover/generated-prompts/sales-tax-ready-prompt.md b/discover/generated-prompts/sales-tax-ready-prompt.md new file mode 100644 index 0000000..fc21b6c --- /dev/null +++ b/discover/generated-prompts/sales-tax-ready-prompt.md @@ -0,0 +1,269 @@ +**Role:** You are a Principal Software Engineer specializing in QuickBooks Online Sales Tax integrations using the QBO Indirect Tax GraphQL API. + +**Context:** I am developing a `python` application using `hints (dataclasses)` typing that needs to calculate sales tax for sales transactions created **outside QuickBooks Online** (e.g. a custom invoicing UI, checkout flow, quote/estimate tool, or third-party ERP). The QBO Indirect Tax GraphQL API will provide accurate, jurisdiction-aware sales tax for each transaction. Assume the application already has a valid OAuth 2.0 access token, QBO realmId (Company ID), and environment (production or sandbox) available as environment variables (`QBO_ACCESS_TOKEN`, `QBO_REALM_ID`, `QBO_ENV`). Focus strictly on the API integration logic. + +**References:** +- Sales Tax documentation: `https://developer.intuit.com/app/developer/gql/docs/api/qbexternal/queries/tax` +- GraphQL schema reference: `https://developer.intuit.com/app/developer/gql/docs/api/qbexternal/queries/` +- OAuth 2.0 documentation: `https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization/oauth-2.0` +- Official sample apps: `https://github.com/IntuitDeveloper/SampleApp-SalesTax-Java` · `https://github.com/IntuitDeveloper/SampleApp-SalesTax-Python` · `https://github.com/IntuitDeveloper/SampleApp-SalesTax-NodeJS` + +--- + +## Use case: Invoicing outside of QuickBooks Online + +This is the documented use case for the Sales Tax GraphQL API. Your app builds the invoice/quote/order in your own UI and database, but uses QBO to compute correct, jurisdiction-aware sales tax for the transaction. The mutation is **stateless** — it computes the tax and returns it without persisting anything in QBO (the returned `taxCalculation.id` will be `"0"`). + +--- + +## Required OAuth Scope + +Ensure your app is authorized with the following scope: +- `indirect-tax.tax-calculation.quickbooks` — required to call the Sales Tax calculation mutation + +> ⚠️ This scope is **separate** from `com.intuit.quickbooks.accounting`. If you only have the accounting scope, the mutation will fail with a `403`. Re-auth your app with the indirect-tax scope before proceeding. + +--- + +## How the Sales Tax API actually works + +The QBO Indirect Tax GraphQL API exposes **one mutation** — there is no separate "discovery" step to fetch tax codes or rates up front. You hand the mutation a transaction (customer + ship-to/ship-from addresses + line items) and it returns the calculated tax broken down by jurisdiction. The applicable rates are derived server-side from the addresses and the company's nexus settings. + +- **Endpoint:** `https://qb.api.intuit.com/graphql` (production) — use `https://qb-sandbox.api.intuit.com/graphql` when `QBO_ENV=sandbox`. +- **HTTP method:** `POST` with `Content-Type: application/json` +- **Required headers:** `Authorization: Bearer `, `realmId: ` +- **Recommended header:** `intuit_tid: ` — echoed back in the response so you can correlate logs with Intuit support. + +--- + +## Task 1: Calculate sales tax via GraphQL mutation + +Call `indirectTaxCalculateSaleTransactionTax` with the transaction inputs. Use the query verbatim below. + +### Mutation + +```graphql +mutation CalculateTax($input: IndirectTax_TaxCalculationInput!) { + indirectTaxCalculateSaleTransactionTax(input: $input) { + ... on IndirectTax_TaxCalculationPayload { + taxCalculation { + id + transactionDate + subject { + customer { id } + taxExemption + } + shipping { + shipFromAddress { streetAddressLine1 } + shipToAddress { streetAddressLine1 } + shippingFee { value } + taxAmount { value } + } + lineItems { + nodes { + numberOfUnits + totalPriceExcludingTaxes { value } + productVariantTaxability { classificationCode } + taxAmount { value } + taxDetails { + taxAmount { value } + taxableAmount { value } + taxExemptAmount { value } + ratePercentageApplied { ... on IndirectTax_RatePercentage { rate } } + taxRate { name taxRate { taxRateReferenceId } } + } + } + } + taxTotals { + totalTaxAmountExcludingShipping { value } + aggregatedTaxesExcludingShippingByRate { + taxableAmount { value } + taxAmount { value } + ratePercentageApplied { ... on IndirectTax_RatePercentage { rate } } + taxRate { name taxRate { taxRateReferenceId } } + } + } + } + } + } +} +``` + +### Variables (example shape — replace with your real transaction inputs) + +```json +{ + "input": { + "transactionDate": "2026-05-21", + "subject": { "qbCustomerId": "1" }, + "shipping": { + "shipFromAddress": { "freeFormAddressLine": "2700 Coast Ave CA, US 94043" }, + "shipToAddress": { "freeFormAddressLine": "2700 Coast Ave CA, US 94043" }, + "shippingFee": { "value": 10.99 } + }, + "lineItems": [ + { + "numberOfUnits": 1, + "pricePerUnitExcludingTaxes": { "value": 100 }, + "productVariantTaxability": { "productVariantId": "1" } + } + ] + } +} +``` + +### Input field reference (from the schema) + +`IndirectTax_TaxCalculationInput`: +- `transactionDate` (Date) — date of the transaction (RFC 3339 full-date, e.g. `"2026-05-21"`). +- `subject` (required) — `{ qbCustomerId: "" }`. Drives any tax-exemption logic. The customer record's tax-exempt status is honored server-side; the response echoes back `subject.taxExemption`. +- `shipping` (required) — `shipFromAddress`, `shipToAddress`, and optional `shippingFee`. Each address (`IndirectTax_AddressInput`) accepts either: + - **Free-form:** `{ freeFormAddressLine: "2700 Coast Ave CA, US 94043" }` — the server parses it. + - **Structured:** `{ streetAddressLine1, city, region, postalCode }` — use this when your app already has address parts. +- `lineItems` (required) — array of `IndirectTax_TaxCalculationLineInput`. **Empty array `[]` is allowed; `null` is not.** Each line accepts: + - `numberOfUnits` (Int) — quantity. + - `pricePerUnitExcludingTaxes: { value }` (Money) — price per unit, tax exclusive. Use **either** this OR `totalPriceExcludingTaxes`. + - `totalPriceExcludingTaxes: { value }` (Money) — line subtotal, tax exclusive (alternative to per-unit pricing). + - `productVariantTaxability: { productVariantId }` (optional) — the QBO Item/product ID for tax classification. Accepts the ID as either a quoted string (`"1"`) or an unquoted integer (`1`) — both verified live. If omitted, the server uses a generic taxability classification. + +### Response shape (what to read back) + +- `taxCalculation.id` — always `"0"` for stateless calculations (nothing was persisted in QBO). +- `taxCalculation.subject.customer.id` — echo of the customer ID you sent. +- `taxCalculation.subject.taxExemption` — exemption status applied (`"NOT_EXEMPT"` is the common case; other values appear when the customer has an exemption certificate on file). +- `taxCalculation.taxTotals.totalTaxAmountExcludingShipping.value` — the headline tax total to display. +- `taxCalculation.taxTotals.aggregatedTaxesExcludingShippingByRate[]` — per-jurisdiction breakdown (`taxRate.name`, `ratePercentageApplied.rate`, `taxAmount.value`, `taxableAmount.value`). +- `taxCalculation.lineItems.nodes[]` — per-line results. **Note:** if you passed a `shippingFee`, the response includes shipping as its own line node with `productVariantTaxability.classificationCode = "SHIPPING"` — iterate carefully. +- Within each line: `taxDetails[]` lists each contributing rate with `taxAmount`, `taxableAmount`, and (when relevant) `taxExemptAmount`. +- `taxRate.taxRate.taxRateReferenceId` — the canonical reference for the rate (useful for audit logs). +- `productVariantTaxability.classificationCode` — the EUC tax classification the server applied (e.g. `"EUC-09020802-V1-00120000"`). Surface this when auditors ask which classification was used. + +### Display a summary + +``` +Customer: (exemption: ) +Ship-to: +Transaction date: + +Per-line tax: + Line 1 (): $ tax=$ + ... + +Tax breakdown by jurisdiction: + % $ + % $ + ... +Total tax (excl. shipping): $ +Shipping tax: $ +``` + +--- + +## Task 2: Verify the result + +- Check `data.indirectTaxCalculateSaleTransactionTax.taxCalculation.taxTotals.totalTaxAmountExcludingShipping.value` is non-null. +- Sum `aggregatedTaxesExcludingShippingByRate[].taxAmount.value` and assert it matches the total (within $0.01 for rounding). +- **Watch for the misleading "NO TAX SALES" case:** when a realm has Sales Tax flags on but no configured nexus, the API returns HTTP 200 with `totalTaxAmountExcludingShipping = 0` and a single rate entry with `taxRate.name = "NO TAX SALES"` and `rate = 0`. This is **not** an error — but generated code should detect this case and surface a clearer message to the user (e.g. "This QBO company has no tax nexus configured; sales tax will be $0"). + +--- + +## Technical Best Practices: + +- **No discovery caching needed.** Unlike most QBO entity flows, there's nothing to cache up front — the mutation computes everything per-call from the addresses you pass. +- **`realmId` is NOT validated against the access token.** Empirically (verified May 2026): passing a bogus `realmId` header with a valid token returns a successful tax calculation against the token's bound company. Do **not** rely on the API to reject mismatched realm IDs. Validate the `QBO_REALM_ID` env var matches the token holder's company in your own app code if this matters. +- **The gateway returns XML for auth errors, JSON for everything else.** A `401` body looks like `AuthenticationFailed...`. Don't blindly `JSON.parse(response.body)` — branch on HTTP status first. +- **Inclusive vs. exclusive pricing.** The schema supports both modes (`pricePerUnitExcludingTaxes` and `pricePerUnitIncludingTaxes`). This prompt's generated code uses **exclusive** pricing — the documented and most common path for US sales tax. If your business model needs inclusive pricing (common in VAT regimes), substitute `pricePerUnitIncludingTaxes` on each line item. +- **Error Handling (empirically verified):** + - **HTTP `401`** (XML body) — token invalid/expired. Surface: "Access token invalid or expired. Please re-authenticate via OAuth 2.0." + - **HTTP `403`** — missing scope (most likely `indirect-tax.tax-calculation.quickbooks`) or the company isn't entitled. Surface: "App is missing the `indirect-tax.tax-calculation.quickbooks` scope, or Sales Tax is not enabled for this company." + - **HTTP `400`** with `extensions.code = "GRAPHQL_VALIDATION_FAILED"` — code bug. You queried a field that doesn't exist or sent a malformed query. Log the full response body, the query, and the variables. Do **not** retry. + - **HTTP `200`** with `data: null` and `errors[]` having `extensions.classification = "ValidationError"` — bad variable input (e.g. invalid `transactionDate` format). Surface `errors[0].message`. + - **HTTP `200`** with `data.indirectTaxCalculateSaleTransactionTax: null` and `errors[]` having `extensions.errorType = "INTERNAL"` — service exception (e.g. missing `subject`). The server returns no numeric code here — match on the `errors[0].message` string itself or fall back to a generic "Tax calculation failed" message. + - **HTTP `200`** with successful `data` but `taxRate.name = "NO TAX SALES"` — see Task 2; surface as "no nexus configured." + - **HTTP `200`** with successful `data` and real rates — calculation succeeded. Note: invalid ship-from/ship-to addresses do **not** raise — the server may silently fall back to the company's configured address. Validate addresses client-side before calling. + - **Network timeout** — retry once with exponential backoff; surface error after the second failure. +- **Documented service error codes.** The Intuit docs list these codes, but the server does **not** consistently return them as machine-readable codes in the GraphQL response — they appear only as substrings in `errors[0].message` when they appear at all. Prefer matching on `extensions.classification` and `extensions.errorType` (above) over chasing numeric codes. + + | Code | Description (from docs) | + |---|---| + | `37138` | Tax group is invalid or has no active associated rates. | + | `37108` | Missing or invalid request to tax calculation. | + | `37111` | Tax API failed; often means no nexus / AST not set up. | + | `37109` | No mapping account for the company (nexus / AST not set up). | + | `19833` | Missing or invalid date input. | + | `19834` | Invalid ship-from address. | + | `19835` | Invalid ship-to address. | + | `19837` | Invalid line amounts. | + +- **Observability:** Capture and log the `intuit_tid` response header on every call. **NEVER** log access tokens, OAuth secrets, or PII (addresses, customer names). +- **Typing:** Provide `hints (dataclasses)` models for `IndirectTax_TaxCalculationInput`, `IndirectTax_TaxCalculationPayload`, `IndirectTax_TaxCalculationLineInput`, and `IndirectTax_ShipmentInput`. +- **Output (integration mode: `new`):** Provide modular, clean code and a runnable verification example. + - **If mode is `new`:** Create a self-contained project in a folder named `qbo-sales-tax-python` (no spaces). Include a `README.md` with setup and environment variable instructions, a dependency manifest, and a runnable main entry point that executes Task 1 against a hardcoded example transaction and prints the breakdown. + - **If mode is `existing`:** Produce modular, well-documented functions/classes/files designed to be imported into an existing codebase. Do **not** scaffold a new project structure. Before writing code, scan the workspace: + 1. Look for a dependency manifest (`pom.xml`, `build.gradle`, `package.json`, `requirements.txt`, `go.mod`, etc.) to confirm the build system. + 2. Look for existing service classes that make QBO API calls (e.g., files containing `QBO_REALM_ID`, `QBO_ACCESS_TOKEN`, `DataService`, or `OAuth2Authorizer`). + 3. Look for any existing tax-related classes (e.g., `TaxService`, `SalesTaxService`). + + State your finding in one sentence before writing code (e.g., "Found existing Express app with `qboClient.js` — adding `salesTaxCalc.js` as a new module.") and match the project's package names, logging style, and error-handling patterns. + +> **SDK note:** No official QBO SDK includes typed bindings for the Indirect Tax GraphQL mutation. Regardless of language, use your preferred HTTP client to POST GraphQL requests to `https://qb.api.intuit.com/graphql` (or `https://qb-sandbox.api.intuit.com/graphql`) — even when an SDK is present, you call this mutation via raw HTTP. + +--- + +## 🛑 AI Guardrails (Anti-Hallucination Constraints) + +**CRITICAL INSTRUCTIONS - YOU MUST ADHERE TO THE FOLLOWING:** +1. **No Hallucinations:** Do not invent or guess GraphQL fields, types, or arguments not present in the mutation/variables provided above. The schema does **not** expose `taxCodes`, `salesTaxCodes`, `taxRates`, or any other root query field for sales tax discovery — only the `indirectTaxCalculateSaleTransactionTax` mutation. +2. **Exact Mutation:** Use the mutation provided in `mutation CalculateTax($input: IndirectTax_TaxCalculationInput!) { + indirectTaxCalculateSaleTransactionTax(input: $input) { + ... on IndirectTax_TaxCalculationPayload { + taxCalculation { + id + transactionDate + subject { + customer { id } + taxExemption + } + shipping { + shipFromAddress { streetAddressLine1 } + shipToAddress { streetAddressLine1 } + shippingFee { value } + taxAmount { value } + } + lineItems { + nodes { + numberOfUnits + totalPriceExcludingTaxes { value } + productVariantTaxability { classificationCode } + taxAmount { value } + taxDetails { + taxAmount { value } + taxableAmount { value } + taxExemptAmount { value } + ratePercentageApplied { ... on IndirectTax_RatePercentage { rate } } + taxRate { name taxRate { taxRateReferenceId } } + } + } + } + taxTotals { + totalTaxAmountExcludingShipping { value } + aggregatedTaxesExcludingShippingByRate { + taxableAmount { value } + taxAmount { value } + ratePercentageApplied { ... on IndirectTax_RatePercentage { rate } } + taxRate { name taxRate { taxRateReferenceId } } + } + } + } + } + } +}` verbatim. Do not add, remove, or rename fields. If the caller doesn't need a field in the response, you may omit it — but do not invent new fields. +3. **No REST V3 / AST:** This prompt is **GraphQL-only**. Do **NOT** generate code that calls the REST V3 `/v3/company/{realmId}/{invoice,salesreceipt,...}` endpoints, queries `TaxCode` / `TaxRate` / `TaxAgency` via REST, or applies `TxnTaxDetail.TxnTaxCodeRef` in a transaction payload. That's a different integration path and is out of scope. +4. **Strict SDK Usage:** Do not call any "tax discovery" SDK method that purports to list tax codes or rates via GraphQL — the schema does not support it. If you find such a method in an SDK, it is targeting the REST V3 endpoints, not GraphQL — and we explicitly do not use REST V3 here. +5. **Endpoint Strictness:** Use the exact endpoints provided. Do not modify base URLs or invent path segments. +6. **Realm ID Header:** Always pass `realmId: $QBO_REALM_ID` as an HTTP header on the GraphQL request. Do not attempt to embed the realm ID inside the GraphQL query (the schema has no `company(id:)` wrapper). +7. **Scope Discipline:** Verify the app holds `indirect-tax.tax-calculation.quickbooks` before calling. Do not assume `com.intuit.quickbooks.accounting` covers this mutation — it does not. +8. **No Discovery Caching:** Do not generate code that "caches tax codes for 1 hour" or pre-fetches a tax code list — there is nothing to pre-fetch. +9. **Stop if Blocked:** If the provided documentation lacks required fields, STOP and clearly state what is missing instead of guessing. + +I have provided you with all the necessary context and instructions. Please generate the code and documentation as per the instructions. diff --git a/discover/instructions.md b/discover/instructions.md index c406f63..c7d9fba 100644 --- a/discover/instructions.md +++ b/discover/instructions.md @@ -45,8 +45,29 @@ Examples of {{transaction_creation_instructions}} that can be used to configure } +Configuring the Custom Fields prompt (choice 3 in merge-prompt.js): +---------------------------- +The Custom Fields prompt has ONE free-form field you configure: + custom_field_transaction_creation_instructions — how to create the transaction that carries the custom field values (default item/customer/amount, which DefinitionId to attach). Reuses {{type_of_transaction}}. +Everything else (GraphQL queries, mutations, payload structure, scope) is a static field maintained to match the API surface — do not change it. +Notes: + - Silver+ partner tier required — Builder-tier apps receive 403 Forbidden. + - The Custom Fields GraphQL API is production-only (no documented sandbox endpoint); test with free non-expiring partner test accounts. REST V3 read/write calls still support sandbox. + - REST V3 payloads must set DefinitionId = legacyIDV2 (the numeric id from the GraphQL definition), not the opaque id. + +Configuring the Sales Tax prompt (choice 4 in merge-prompt.js): +---------------------------- +The Sales Tax prompt has NO free-form configurable fields — it is driven entirely by static fields (the calculate mutation, its variables example, scope, and documentation links). +Notes: + - Requires the OAuth scope: indirect-tax.tax-calculation.quickbooks + - Calculation-only — the API returns a tax calculation; it does not create or post a transaction. + + ### Author, use-case, API version, last-tested date (Template registries) Intuit Developer, Dimensions API, v1, 2026-03-30 + Intuit Developer, Projects API, v1, 2026-03-30 + Intuit Developer, Custom Fields API, v1, 2026-07-16 + Intuit Developer, Sales Tax API, v1, 2026-07-16 diff --git a/discover/merge-prompt.js b/discover/merge-prompt.js index 9a0b56d..3d7cd67 100644 --- a/discover/merge-prompt.js +++ b/discover/merge-prompt.js @@ -9,43 +9,6 @@ if (configFlagIndex !== -1 && args[configFlagIndex + 1]) { configFile = args[configFlagIndex + 1]; } -// --language selects which SDK notes file (if any) to inject into the -// {{sdk_notes}} placeholder. Falls back to language_framework from the config. -let language = null; -const languageFlagIndex = args.indexOf('--language'); -if (languageFlagIndex !== -1 && args[languageFlagIndex + 1]) { - language = args[languageFlagIndex + 1]; -} - -// Normalize a language name to its sdk-notes filename stem (e.g. "C#" -> "dotnet"). -function normalizeLanguage(lang) { - if (!lang) return null; - // Map "c#" -> "csharp" before stripping symbols so it doesn't collapse to "c". - const key = String(lang).toLowerCase().replace(/#/g, 'sharp').replace(/[^a-z0-9]/g, ''); - const aliases = { - py: 'python', python: 'python', python3: 'python', - ts: 'nodejs', typescript: 'nodejs', js: 'nodejs', javascript: 'nodejs', node: 'nodejs', nodejs: 'nodejs', - dotnet: 'dotnet', net: 'dotnet', csharp: 'dotnet', cs: 'dotnet', - java: 'java', - php: 'php', - ruby: 'ruby', rb: 'ruby', - }; - return aliases[key] || key; -} - -// Display name + default typing system for a normalized language. Used to keep -// the whole prompt consistent when --language overrides the config default. -function languageProfile(normalized) { - const profiles = { - python: { framework: 'Python3', typing: 'hints (dataclasses)' }, - java: { framework: 'Java', typing: 'Java classes/records' }, - dotnet: { framework: '.NET (C#)', typing: 'C# classes/records' }, - nodejs: { framework: 'TypeScript', typing: 'TypeScript interfaces' }, - php: { framework: 'PHP', typing: 'PHP 8 typed properties' }, - ruby: { framework: 'Ruby', typing: 'Sorbet type annotations' }, - }; - return profiles[normalized] || null; -} if (!fs.existsSync(configFile)) { console.error(`Error: Config file "${configFile}" not found.`); process.exit(1); @@ -68,54 +31,11 @@ if (fs.existsSync(schemaPath)) { console.warn('Warning: prompt-config.schema.json not found, skipping validation.'); } -// Resolve the SDK notes for a template. Notes live in a `sdk-notes/` folder next -// to the template (e.g. custom-fields/sdk-notes/python.md). Returns the notes -// text, or an empty string when no notes apply (so {{sdk_notes}} never leaks). -function resolveSdkNotes(templateFile) { - // Language precedence: --language flag, then language_framework from config. - const lang = normalizeLanguage(language || config.language_framework); - if (!lang) return ''; - - const notesDir = path.join(__dirname, path.dirname(templateFile), 'sdk-notes'); - if (!fs.existsSync(notesDir)) { - return ''; // This prompt has no SDK notes at all — templates handle this. - } - - const notesFile = path.join(notesDir, `${lang}.md`); - if (!fs.existsSync(notesFile)) { - const available = fs.readdirSync(notesDir) - .filter(f => f.endsWith('.md')) - .map(f => f.replace(/\.md$/, '')); - console.warn( - `\nWarning: no SDK notes for language "${lang}" in ${notesDir}.` + - `\n Available: ${available.join(', ') || '(none)'}. Leaving SDK notes section empty.` - ); - return ''; - } - - console.log(`Injecting SDK notes: ${path.relative(__dirname, notesFile)}`); - return fs.readFileSync(notesFile, 'utf8').trim(); -} - // --- Merge helper (multi-pass to resolve nested placeholders) --- function mergeTemplate(templateFile, outputFile) { let prompt = fs.readFileSync(templateFile, 'utf8'); - // sdk_notes is not a config key — it's loaded from a per-prompt notes file. - // Merge it alongside the config values so {{sdk_notes}} always resolves. - const mergeValues = { ...config, sdk_notes: resolveSdkNotes(templateFile) }; - - // When --language is explicitly passed, override the language fields too, so the - // prompt's Context line and typing match the injected SDK notes (otherwise the - // prompt contradicts itself). With no flag, config values are used unchanged. - if (language) { - const profile = languageProfile(normalizeLanguage(language)); - if (profile) { - mergeValues.language_framework = profile.framework; - mergeValues.typing_system = profile.typing; - console.log(`Language override: language_framework="${profile.framework}", typing_system="${profile.typing}"`); - } - } + const mergeValues = { ...config }; const MAX_PASSES = 5; for (let pass = 0; pass < MAX_PASSES; pass++) { diff --git a/discover/projects/prompt-template-projects.md b/discover/projects/prompt-template-projects.md index fd530f4..3920033 100644 --- a/discover/projects/prompt-template-projects.md +++ b/discover/projects/prompt-template-projects.md @@ -2,6 +2,12 @@ **Context:** I am developing a `{{language_framework}}` application. I need to implement a workflow that uses the Projects APIs to create a project and generate project estimates for IES or QuickBooks Advanced companies. Assume the application already has a valid OAuth 2.0 access token, realmId (Company ID), and environment (production or sandbox) available in the `.env` file. Focus strictly on the API integration logic. +**References:** +- Project Estimate documentation: `{{project-estimate-documentation}}` +- GraphQL schema reference: `{{graphql_schema}}` +- REST V3 API documentation: `{{rest_v3_api_documentation}}` +- OAuth 2.0 documentation: `{{oauth2-documentation}}` + --- ## Task 1: Pre-flight & Discovery @@ -83,7 +89,7 @@ Ensure the Estimate request body includes **both** `ProjectRef` and `CustomerRef At line level, ensure each line item includes both `Amount` and `CostAmount`, where `CostAmount` is calculated as `Amount` plus/minus `{{markup_percentages}}` percent of `Amount`. **Constraints:** -- Use `minorversion=75` to ensure `ProjectRef` is processed. +- Use `minorversion={{minorversion}}` to ensure `ProjectRef` is processed. --- @@ -116,7 +122,7 @@ Once the Estimate is created, display it to the user in a readable format. 1. **No Hallucinations:** Do not invent, guess, or hallucinate API endpoints, GraphQL properties, or SDK methods that are not explicitly provided in the context or linked documentation. 2. **Strict SDK/Library Usage:** If an official SDK or library is specified (e.g., Intuit Java SDK), use ONLY the methods and classes that exist in its latest public release. Do not construct fake SDK models. 3. **Provided Links Only:** You must derive all API syntax, structure, and constraints strictly from the provided links. All HTTP responses (GraphQL and REST) must be parsed according to the provided documentation. -4. **Endpoint Strictness:** Use the exact endpoints and query structures provided. Do not attempt to modify the base URL, append unsupported parameters, or alter the `minorversion=75` requirement. +4. **Endpoint Strictness:** Use the exact endpoints and query structures provided. Do not attempt to modify the base URL, append unsupported parameters, or alter the `minorversion={{minorversion}}` requirement. 5. **If Blocked/Missing Info:** If the provided documentation or payload structures lack required fields to compile a functional request, STOP and clearly state what specific information is missing instead of making an educated guess. I have provided you with all the necessary context and instructions. Please generate the code and documentation as per the instructions. \ No newline at end of file diff --git a/discover/prompt-config.json b/discover/prompt-config.json index 99a408b..e805afd 100644 --- a/discover/prompt-config.json +++ b/discover/prompt-config.json @@ -19,7 +19,7 @@ "company_preferences_rest_v3_api_endpoint": "GET /v3/company/{{companyid}}/query?minorversion={{minorversion}}&query=select * from preferences", "dimension_Api_documentation": "https://developer.intuit.com/app/developer/ies/docs/workflows/manage-dimensions/dimensions-use-cases", "project-estimate-documentation": "https://developer.intuit.com/app/developer/qbo/docs/workflows/manage-projects/use-cases#use-case-5", - "rest_v3_api_documentation": "https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/account", + "rest_v3_api_documentation": "https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/{{type_of_transaction}}", "dimension_discovery_query": "query ActiveCustomDimensionDefinitionsQuery {\n appFoundationsActiveCustomDimensionDefinitions(\n first: 20\n after: null\n last: null\n before: null\n ) {\n edges {\n node {\n ...CustomDimensionDefinitionAttributes\n }\n }\n }\n}\nfragment CustomDimensionDefinitionAttributes on AppFoundations_CustomDimensionDefinition {\n id\n label\n active\n}", "dimension_values_query": "query CustomDimensionsValuesQuery {\n appFoundationsActiveCustomDimensionValues(\n first: 20,\n filters: { definitionId: \"1000000002\", parentId: null }\n ) {\n edges {\n node {\n id\n definitionId\n fullyQualifiedLabel\n }\n }\n }\n}", "projects_discovery_query": "{\"query\":\"query projectManagementProjects($first: PositiveInt!,$after: String,$filter: ProjectManagement_ProjectFilter!,$orderBy: [ProjectManagement_OrderBy!]){projectManagementProjects(first: $first,after: $after,filter: $filter,orderBy: $orderBy){edges{node{id,name,status,dueDate,customer{id},account{id}}}pageInfo{hasNextPage,hasPreviousPage,startCursor,endCursor}}}\",\"variables\":{\"first\":4,\"filter\":{\"status\":{\"in\":[\"OPEN\",\"IN_PROGRESS\"]}},\"orderBy\":[\"DUE_DATE_ASC\"]}}", diff --git a/discover/prompt-config.schema.json b/discover/prompt-config.schema.json index 8f25d6b..a49dcd9 100644 --- a/discover/prompt-config.schema.json +++ b/discover/prompt-config.schema.json @@ -175,6 +175,82 @@ "java-sdk-documentation": { "type": "string", "description": "URL to Java SDK installation documentation" + }, + "custom_field_documentation": { + "type": "string", + "description": "URL to the Custom Fields use-case documentation" + }, + "custom_field_scope_readwrite": { + "type": "string", + "description": "OAuth scope required for read/write access to custom field definitions" + }, + "custom_field_definitions_query": { + "type": "string", + "description": "GraphQL query for fetching custom field definitions" + }, + "custom_field_create_definition_mutation": { + "type": "string", + "description": "GraphQL mutation for creating a custom field definition" + }, + "custom_field_create_definition_variables_example": { + "type": "string", + "description": "Example variables payload for the create custom field definition mutation" + }, + "custom_field_update_definition_mutation": { + "type": "string", + "description": "GraphQL mutation for updating a custom field definition" + }, + "custom_field_update_definition_variables_example": { + "type": "string", + "description": "Example variables payload for the update custom field definition mutation" + }, + "custom_field_disable_variables_example": { + "type": "string", + "description": "Example variables payload for disabling a custom field definition" + }, + "custom_field_payload_structure": { + "type": "string", + "description": "JSON shape of the custom field values attached to a transaction" + }, + "custom_field_transaction_creation_instructions": { + "type": "string", + "description": "Free-form instructions for creating a transaction with custom field values" + }, + "custom_field_sample_app_java": { + "type": "string", + "description": "URL to the official Custom Fields Java sample app" + }, + "custom_field_sample_app_python": { + "type": "string", + "description": "URL to the official Custom Fields Python sample app" + }, + "sales_tax_documentation": { + "type": "string", + "description": "URL to the Sales Tax use-case documentation" + }, + "sales_tax_scope": { + "type": "string", + "description": "OAuth scope required for the Sales Tax calculation API" + }, + "sales_tax_calculate_mutation": { + "type": "string", + "description": "GraphQL mutation for calculating sale transaction tax" + }, + "sales_tax_calculate_variables_example": { + "type": "string", + "description": "Example variables payload for the sales tax calculation mutation" + }, + "sales_tax_sample_app_java": { + "type": "string", + "description": "URL to the official Sales Tax Java sample app" + }, + "sales_tax_sample_app_python": { + "type": "string", + "description": "URL to the official Sales Tax Python sample app" + }, + "sales_tax_sample_app_nodejs": { + "type": "string", + "description": "URL to the official Sales Tax Node.js sample app" } }, "additionalProperties": false diff --git a/discover/sales-tax/README.md b/discover/sales-tax/README.md deleted file mode 100644 index 7683748..0000000 --- a/discover/sales-tax/README.md +++ /dev/null @@ -1,148 +0,0 @@ -# Sales Tax Prompt - -Generates AI-ready prompts that produce a runnable integration for the **QuickBooks Online Sales Tax GraphQL API** — specifically the `indirectTaxCalculateSaleTransactionTax` mutation. The generated code calculates jurisdiction-aware sales tax for transactions your app builds **outside** QuickBooks Online (custom invoicing UI, checkout flow, quote tool, third-party ERP). - -> ℹ️ **GraphQL only.** This prompt does **not** generate REST V3 / Automated Sales Tax (AST) integration code. The Sales Tax mutation is stateless — it calculates tax without persisting anything in QBO. - -## Folder Contents - -``` -sales-tax/ -├── README.md # This file -├── prompt-template-sales-tax.md # The prompt template (do not edit unless updating the workflow) -├── postman/ # Reference Postman collection for manual testing -└── sdk-notes/ # Per-language SDK guidance, injected at merge time - ├── dotnet.md - ├── java.md - ├── nodejs.md - ├── php.md - ├── python.md - └── ruby.md -``` - -The template lives here; the merge script and config live one level up in `discover/`. - -## Use case covered - -**Invoicing outside of QuickBooks Online.** Your app builds the transaction (cart, quote, invoice draft, etc.) in your own UI and storage, then calls the QBO Sales Tax mutation to get accurate jurisdiction-aware tax for that transaction. The mutation returns the calculated tax and walks away — nothing is persisted in QBO. - -## What the generated code does - -| Task | Layer | Purpose | -|---|---|---| -| 1. Calculate | GraphQL | Call `indirectTaxCalculateSaleTransactionTax` with customer + ship-from/ship-to addresses + line items. Receive total tax + per-jurisdiction breakdown + per-line breakdown. | -| 2. Verify | GraphQL response | Confirm `totalTaxAmountExcludingShipping` is present, sum per-rate amounts, detect the "NO TAX SALES" no-nexus edge case, and print a human-readable breakdown. | - -## What's covered in the generated code - -- The full mutation with all useful response fields: tax totals, per-jurisdiction breakdown, per-line breakdown, customer exemption status, EUC classification codes, tax rate reference IDs. -- Exclusive pricing (`pricePerUnitExcludingTaxes`) — the documented US sales tax path. -- Empty `lineItems: []` accepted (returns zero tax). -- Free-form addresses (`freeFormAddressLine`) — the API parses them server-side. -- Shipping fees calculated as their own line item in the response. -- Tax-exemption status echoed back via `subject.taxExemption` (the customer's exemption is honored automatically server-side). -- Error handling for the documented gateway and service error patterns, plus empirically verified quirks. - -## What's NOT covered (out of scope) - -- **REST V3 / Automated Sales Tax (AST)** — see Intuit's separate [AST documentation](https://developer.intuit.com/app/developer/qbo/docs/workflows/manage-sales-tax-for-us-locales) if you need to persist transactions directly in QBO and let QBO compute tax. -- **Inclusive pricing** (`pricePerUnitIncludingTaxes`) — schema supports it but the prompt generates the exclusive path. Swap the field in the generated code if you need VAT-style inclusive pricing. -- **Tax-exemption certificate management** — the API honors a customer's existing exemption status, but creating/managing the exemption certificate itself is a separate workflow. -- **Non-US locales** — the GraphQL mutation works internationally, but the prompt's examples and error guidance are US-centric. -- **Multi-realm / multi-tenant routing** — the generated code reads one realm from one env var. - -## Authentication & `.env` Setup - -The generated code assumes the caller already has a valid **OAuth 2.0 access token** and **realmId** available in a `.env` file. The prompt is intentionally scoped to API integration logic — it does **not** generate an OAuth client. - -### Required `.env` variables - -```bash -QBO_ACCESS_TOKEN= -QBO_REALM_ID= -QBO_ENV=production # or "sandbox" -``` - -### Environment endpoints - -| Env | GraphQL host | -|---|---| -| Production | `https://qb.api.intuit.com/graphql` | -| Sandbox | `https://qb-sandbox.api.intuit.com/graphql` | - -### How to get an access token - -Use the **[Intuit OAuth 2.0 Playground](https://developer.intuit.com/app/developer/playground)** to generate an access token (sandbox or production) with the required scope (see below). Paste the token into `.env` and run the generated app. - -### Required OAuth Scope - -| Scope | Used For | -|---|---| -| `indirect-tax.tax-calculation.quickbooks` | Calling the Sales Tax calculation mutation | - -> ⚠️ `com.intuit.quickbooks.accounting` is **not** sufficient. The Sales Tax mutation requires the dedicated `indirect-tax.tax-calculation.quickbooks` scope. A missing scope surfaces as `403`. - -## Important: There Is No Tax Code "Discovery" Step - -Unlike most QBO entity flows, the Sales Tax GraphQL API does **not** expose root queries to list tax codes or tax rates. The mutation derives the applicable rates server-side from the addresses you pass in and the company's nexus configuration. **Do not** add code that pre-fetches a tax-code list before calling the mutation — the schema does not support it (`taxCodes`, `salesTaxCodes`, `taxRates`, and similar root fields do not exist and will return `GRAPHQL_VALIDATION_FAILED`). - -## Generating a Prompt - -From the `discover/` directory: - -```bash -# Default (uses prompt-config.json) -node merge-prompt.js - -# Language-specific (selects the right sdk-notes/.md) -node merge-prompt.js --language java -node merge-prompt.js --language python -node merge-prompt.js --language dotnet -``` - -Choose option **4** at the menu prompt. Output is written to `generated-sample-prompts/sales-tax-ready-prompt.md`. Paste that file into your AI coding assistant (Copilot, Cursor, ChatGPT, Windsurf) to scaffold the integration. - -## SDK Notes - -The `sdk-notes/` folder contains per-language guidance that is injected into the prompt at merge time via the `{{sdk_notes}}` placeholder. If you run `--language java`, only `sdk-notes/java.md` is included. None of the official QBO SDKs include typed bindings for the Indirect Tax GraphQL mutation — even when an SDK is present, you call this mutation via raw HTTP. - -## Common Errors - -| HTTP | Response shape | Cause | -|---|---|---| -| `200` | `data.indirectTaxCalculateSaleTransactionTax.taxCalculation` populated, no `errors[]` | Success — check for the `NO TAX SALES` no-nexus case below | -| `200` | `data: null`, `errors[].extensions.classification = "ValidationError"` | Variable input fails schema validation (e.g. bad `transactionDate` RFC3339 format). Surface `errors[0].message`. | -| `200` | `data.indirectTaxCalculateSaleTransactionTax: null`, `errors[].extensions.errorType = "INTERNAL"` | Service exception — e.g. missing `subject`. Server does not return a numeric code; match on the message string. | -| `400` | JSON with `errors[].extensions.code = "GRAPHQL_VALIDATION_FAILED"` | Code bug — you queried a field that doesn't exist on the schema. Do not retry. | -| `401` | **XML** with `AuthenticationFailed` | Token invalid or expired. Refresh in the [OAuth Playground](https://developer.intuit.com/app/developer/playground). | -| `403` | XML or JSON depending on layer | Missing `indirect-tax.tax-calculation.quickbooks` scope, or Sales Tax not entitled on the company | - -### Empirical quirks (verified against production, May 2026) - -| Behavior | Implication | -|---|---| -| `realmId` header is **silently ignored** if the access token is valid | Do not rely on the API to enforce realm match. If your app must talk to a specific realm, verify it in your own code. | -| Invalid ship-from / ship-to address (e.g. "gibberish nowhere") | Server **does not error** — silently falls back to a default address (observed: Georgia / Atlanta). Validate addresses client-side. | -| Empty `lineItems: []` | Accepted by the schema; returns a zero-tax calculation. | -| Realm with Sales Tax flags on but **no nexus configured** | Returns HTTP 200 with `totalTaxAmountExcludingShipping = 0` and a single rate `taxRate.name = "NO TAX SALES"` (`rate = 0`). **Not** an error — but generated code should detect this and surface a clearer message to the end user. | -| Documented service error codes (`19833`, `19834`, `19835`, `19837`, `37108`, `37109`, `37111`, `37138`) | Listed in Intuit docs but **not consistently returned as machine-readable codes** in the GraphQL response. Prefer matching on `extensions.classification` / `extensions.errorType` over chasing numeric codes. | - -## Worked Example - -Calling the mutation with a $100 product shipped within Mountain View, CA (94043), against a realm with nexus configured in California: - -``` -Total tax: $9.75 - California State 6.25% $6.25 - California, Santa Clara County 1.00% $1.00 - California, Santa Clara County District 2.50% $2.50 -``` - -Same payload against a realm with no nexus configured: - -``` -Total tax: $0.00 - NO TAX SALES 0.00% $0.00 -``` - -Both responses are HTTP 200. Generated code should distinguish these for the end user. diff --git a/discover/sales-tax/prompt-template-sales-tax.md b/discover/sales-tax/prompt-template-sales-tax.md index a0bb8c5..d7f0474 100644 --- a/discover/sales-tax/prompt-template-sales-tax.md +++ b/discover/sales-tax/prompt-template-sales-tax.md @@ -107,7 +107,7 @@ Shipping tax: $ --- -## Technical Best Practices +## Technical Best Practices: - **No discovery caching needed.** Unlike most QBO entity flows, there's nothing to cache up front — the mutation computes everything per-call from the addresses you pass. - **`realmId` is NOT validated against the access token.** Empirically (verified May 2026): passing a bogus `realmId` header with a valid token returns a successful tax calculation against the token's bound company. Do **not** rely on the API to reject mismatched realm IDs. Validate the `QBO_REALM_ID` env var matches the token holder's company in your own app code if this matters. @@ -138,7 +138,7 @@ Shipping tax: $ - **Observability:** Capture and log the `intuit_tid` response header on every call. **NEVER** log access tokens, OAuth secrets, or PII (addresses, customer names). - **Typing:** Provide `{{typing_system}}` models for `IndirectTax_TaxCalculationInput`, `IndirectTax_TaxCalculationPayload`, `IndirectTax_TaxCalculationLineInput`, and `IndirectTax_ShipmentInput`. - **Output (integration mode: `{{integration_mode}}`):** Provide modular, clean code and a runnable verification example. - - **If mode is `new`:** Create a self-contained project in a folder named `qbo-sales-tax-{{language_framework}}` (no spaces, lowercase). Include a `README.md` with setup and environment variable instructions, a dependency manifest, and a runnable main entry point that executes Task 1 against a hardcoded example transaction and prints the breakdown. + - **If mode is `new`:** Create a self-contained project in a folder named `qbo-sales-tax-{{language_framework}}` (no spaces). Include a `README.md` with setup and environment variable instructions, a dependency manifest, and a runnable main entry point that executes Task 1 against a hardcoded example transaction and prints the breakdown. - **If mode is `existing`:** Produce modular, well-documented functions/classes/files designed to be imported into an existing codebase. Do **not** scaffold a new project structure. Before writing code, scan the workspace: 1. Look for a dependency manifest (`pom.xml`, `build.gradle`, `package.json`, `requirements.txt`, `go.mod`, etc.) to confirm the build system. 2. Look for existing service classes that make QBO API calls (e.g., files containing `QBO_REALM_ID`, `QBO_ACCESS_TOKEN`, `DataService`, or `OAuth2Authorizer`). @@ -146,19 +146,13 @@ Shipping tax: $ State your finding in one sentence before writing code (e.g., "Found existing Express app with `qboClient.js` — adding `salesTaxCalc.js` as a new module.") and match the project's package names, logging style, and error-handling patterns. ---- - -## Language-Specific SDK Notes - -{{sdk_notes}} - -> If no SDK notes appear above, no official entity SDK exists for your language. Use your preferred HTTP client to POST GraphQL requests to `{{graphql_endpoint_production}}` (or `{{graphql_endpoint_sandbox}}`). The official QBO SDKs do **not** include typed bindings for the Indirect Tax GraphQL mutation — even when an SDK is present, you call this mutation via raw HTTP. +> **SDK note:** No official QBO SDK includes typed bindings for the Indirect Tax GraphQL mutation. Regardless of language, use your preferred HTTP client to POST GraphQL requests to `{{graphql_endpoint_production}}` (or `{{graphql_endpoint_sandbox}}`) — even when an SDK is present, you call this mutation via raw HTTP. --- ## 🛑 AI Guardrails (Anti-Hallucination Constraints) -**CRITICAL INSTRUCTIONS — YOU MUST ADHERE TO THE FOLLOWING:** +**CRITICAL INSTRUCTIONS - YOU MUST ADHERE TO THE FOLLOWING:** 1. **No Hallucinations:** Do not invent or guess GraphQL fields, types, or arguments not present in the mutation/variables provided above. The schema does **not** expose `taxCodes`, `salesTaxCodes`, `taxRates`, or any other root query field for sales tax discovery — only the `indirectTaxCalculateSaleTransactionTax` mutation. 2. **Exact Mutation:** Use the mutation provided in `{{sales_tax_calculate_mutation}}` verbatim. Do not add, remove, or rename fields. If the caller doesn't need a field in the response, you may omit it — but do not invent new fields. 3. **No REST V3 / AST:** This prompt is **GraphQL-only**. Do **NOT** generate code that calls the REST V3 `/v3/company/{realmId}/{invoice,salesreceipt,...}` endpoints, queries `TaxCode` / `TaxRate` / `TaxAgency` via REST, or applies `TxnTaxDetail.TxnTaxCodeRef` in a transaction payload. That's a different integration path and is out of scope. diff --git a/discover/sales-tax/sdk-notes/dotnet.md b/discover/sales-tax/sdk-notes/dotnet.md deleted file mode 100644 index 17ced59..0000000 --- a/discover/sales-tax/sdk-notes/dotnet.md +++ /dev/null @@ -1,10 +0,0 @@ -**If generating .NET / C# code (`{{language_framework}}` = dotnet):** - -The Intuit .NET SDK does **not** support GraphQL. Use a plain HTTP client. - -- Use `System.Net.Http.HttpClient` to POST to `{{graphql_endpoint_production}}` (or `{{graphql_endpoint_sandbox}}` when `QBO_ENV=sandbox`). No extra package needed — `HttpClient` is part of the .NET standard library. -- Required headers: `Authorization: Bearer `, `Content-Type: application/json`, `realmId: `. -- Recommended: set a unique `intuit_tid` header per request for log correlation. -- Use `System.Text.Json` (built-in) or Newtonsoft.Json to serialize variables and parse the response. - -> ℹ️ If your project already depends on `IppDotNetSdkForQuickBooksApiV3` for other QBO work, you can reuse its `OAuth2Authorizer` for token refresh, but you'll bypass `DataService` entirely for the Sales Tax mutation. diff --git a/discover/sales-tax/sdk-notes/java.md b/discover/sales-tax/sdk-notes/java.md deleted file mode 100644 index 78bd4b1..0000000 --- a/discover/sales-tax/sdk-notes/java.md +++ /dev/null @@ -1,11 +0,0 @@ -**If generating Java code (`{{language_framework}}` = java):** - -The Intuit Java SDK (`ipp-v3-java-devkit`) **does not support GraphQL**. Use a plain HTTP client for the Sales Tax mutation. - -- Use `java.net.http.HttpClient` (JDK 11+) or `CloseableHttpClient` from Apache HttpClient. POST to `{{graphql_endpoint_production}}` (or `{{graphql_endpoint_sandbox}}` when `QBO_ENV=sandbox`). -- Required headers: `Authorization: Bearer `, `Content-Type: application/json`, `realmId: `. -- Recommended: set a unique `intuit_tid` header per request for log correlation. -- Use Jackson or Gson to serialize the variables JSON and parse the response. -- **SDK reference (for unrelated REST V3 work):** `{{java-sdk-documentation}}` — note that this SDK is **not** needed for this prompt. - -> ℹ️ If your project already depends on `ipp-v3-java-devkit` for other QBO work, you can reuse its `OAuth2Authorizer` for token refresh, but you'll bypass `DataService` entirely for the Sales Tax mutation. diff --git a/discover/sales-tax/sdk-notes/nodejs.md b/discover/sales-tax/sdk-notes/nodejs.md deleted file mode 100644 index 7ae2f3d..0000000 --- a/discover/sales-tax/sdk-notes/nodejs.md +++ /dev/null @@ -1,8 +0,0 @@ -**If generating Node.js code (`{{language_framework}}` = nodejs):** - -There is no official Intuit entity SDK for Node.js, and the Sales Tax mutation has no SDK bindings regardless. Use plain HTTP. - -- Use `axios` or native `fetch` (Node 18+) to POST to `{{graphql_endpoint_production}}` (or `{{graphql_endpoint_sandbox}}` when `QBO_ENV=sandbox`). -- Required headers: `Authorization: Bearer `, `Content-Type: application/json`, `realmId: `. -- Recommended: set a unique `intuit_tid` header per request for log correlation. -- No additional package required beyond your HTTP client of choice (`npm install axios` if preferred). diff --git a/discover/sales-tax/sdk-notes/php.md b/discover/sales-tax/sdk-notes/php.md deleted file mode 100644 index 7d710eb..0000000 --- a/discover/sales-tax/sdk-notes/php.md +++ /dev/null @@ -1,14 +0,0 @@ -**If generating PHP code (`{{language_framework}}` = php):** - -The PHP SDK does **not** support GraphQL. Use a plain HTTP client. - -- Use `GuzzleHttp\Client` or PHP's built-in `curl` to POST to `{{graphql_endpoint_production}}` (or `{{graphql_endpoint_sandbox}}` when `QBO_ENV=sandbox`). -- Required headers: `Authorization: Bearer `, `Content-Type: application/json`, `realmId: `. -- Recommended: set a unique `intuit_tid` header per request for log correlation. -- Use `json_encode` / `json_decode` for variables and response. -- **Composer install (if using Guzzle):** - ```bash - composer require guzzlehttp/guzzle - ``` - -> ℹ️ If your project already depends on `quickbooks/v3-php-sdk` for other QBO work, you can reuse its OAuth helpers for token refresh, but you'll bypass `DataService` entirely for the Sales Tax mutation. diff --git a/discover/sales-tax/sdk-notes/python.md b/discover/sales-tax/sdk-notes/python.md deleted file mode 100644 index 04b0bd9..0000000 --- a/discover/sales-tax/sdk-notes/python.md +++ /dev/null @@ -1,11 +0,0 @@ -**If generating Python code (`{{language_framework}}` = python):** - -There is no official Intuit entity SDK for Python, and no Python SDK bindings for the Indirect Tax GraphQL mutation regardless. Use plain HTTP. - -- Use `requests` to POST to `{{graphql_endpoint_production}}` (or `{{graphql_endpoint_sandbox}}` when `QBO_ENV=sandbox`). -- Required headers: `Authorization: Bearer `, `Content-Type: application/json`, `realmId: `. -- Recommended: set a unique `intuit_tid` header per request for log correlation. -- **Install:** - ```bash - pip install requests - ``` diff --git a/discover/sales-tax/sdk-notes/ruby.md b/discover/sales-tax/sdk-notes/ruby.md deleted file mode 100644 index a30ba10..0000000 --- a/discover/sales-tax/sdk-notes/ruby.md +++ /dev/null @@ -1,11 +0,0 @@ -**If generating Ruby code (`{{language_framework}}` = ruby):** - -There is no official Intuit entity SDK for Ruby, and no Ruby bindings for the Sales Tax mutation regardless. Use plain HTTP. - -- Use Ruby's built-in `Net::HTTP` or the `faraday` gem to POST to `{{graphql_endpoint_production}}` (or `{{graphql_endpoint_sandbox}}` when `QBO_ENV=sandbox`). -- Required headers: `Authorization: Bearer `, `Content-Type: application/json`, `realmId: `. -- Recommended: set a unique `intuit_tid` header per request for log correlation. -- **Optional install (if using Faraday):** - ```bash - gem install faraday - ```