Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,10 @@ workflows/webhooks-migration/ready-prompt.md

# Node
node_modules/

# Local test harnesses & Postman collections (not distributed with the prompt packages)
discover/*/postman/
discover/*/test/

# Claude Code — local tooling, not shipped in this public repo.
.claude/
141 changes: 99 additions & 42 deletions discover/README.md

Large diffs are not rendered by default.

295 changes: 295 additions & 0 deletions discover/custom-fields/README.md

Large diffs are not rendered by default.

19 changes: 17 additions & 2 deletions discover/custom-fields/prompt-template-custom-fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@
- OAuth 2.0 documentation: `{{oauth2-documentation}}`
- Official sample apps: `{{custom_field_sample_app_java}}` · `{{custom_field_sample_app_python}}`

**Hosts:**
- **GraphQL endpoint** (Tasks 1, 4, 5 — Custom Field *definitions*). This API is **production-only**, so always use the production host regardless of `QBO_ENV`:
- Production: `{{graphql_endpoint_production}}`
- **REST V3 base URL** (Tasks 2, 3 — attaching/reading custom field *values* on a transaction). These honor `QBO_ENV`:
- Production: `https://{{rest_baseurl_production}}`
- Sandbox: `https://{{rest_baseurl_sandbox}}`

REST V3 paths below (e.g. `POST /v3/company/{{companyid}}/...`) are relative to the REST base URL. Do not modify either host.

---

## Task 1: Discover Custom Field Definitions (GraphQL)
Expand Down Expand Up @@ -140,7 +149,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:
Expand All @@ -157,11 +166,17 @@ 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), the deciding constraint is the mandatory `include=enhancedAllCustomFields` URL parameter: use the SDK's create/read methods (`DataService.add()` / `findById()` in Java, `dataService.Add<T>()` 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.
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<T>()` 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.)
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.
Expand Down
90 changes: 90 additions & 0 deletions discover/custom-fields/sdk-notes/dotnet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
**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<string, DefinitionMeta>` 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 `<StringValue>` (for `STRING` / `STRING_LIST` / `OBJECT_LIST`)
- `decimal` → serializes as `<NumberValue>` (for `NUMBER`)
- `DateTime` → serializes as `<DateValue>` (for `DATE`, value is `YYYY-MM-DD`)
- `bool` → serializes as `<BooleanValue>` (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<T>(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<T>(...)` 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<T>()` 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.
Loading