# NetSuite revenue voucher NexusCRM API outbound

This is the selected Design v0.8 route: a separate `pingcap-datapipelines`
Cloud Run Job freezes and validates a NetSuite candidate, performs Account
admission through the NexusCRM query API, then writes only through the
NexusCRM Composite Records API. The pipeline does not have, request, or use a
CRM TiDB connection, database credential, staging table, SQL upsert, or DDL
preflight.

## Boundary and current status

The task is intentionally independent of `extract`, `load`, and
`run-pipeline`. It does not use BigQuery in the write decision. Its dedicated
runtime identity has candidate-GCS access plus Secret Manager references for
NetSuite TBA and a NexusCRM machine client; the Integration User needs only
Describe/query Account and target-object read plus target-object create/update.
It must have neither delete nor database access.

Terraform creates no resources until `enable_netsuite_revenue_to_nexuscrm` is
separately enabled. The ordinary deploy allowlist excludes the Job. If later
provisioned with an approved image digest, its Scheduler remains paused and its
normal arguments omit both `--validate-only` and `--commit`, so it can only
freeze a source candidate.

No Production API connection, secret read, Cloud Run deployment, Scheduler
enablement, or API write is authorized by this repository change. The L2 PR
#5216 is OPEN/BLOCKED and is not evidence that the v0.8 successor object has
been deployed.

## Candidate and Account admission

The RESTlet is fetched in 500-row pages. Each raw page and the complete source
candidate/manifest are immutable under an isolated run prefix. The candidate
validates realm, search, schemas, ordered 35 fields, cursor progression,
stable source key, row count, and control totals before it can reach CRM.
Retries replay that frozen candidate instead of mixing source pages.

After the complete candidate is frozen, the job reads the source Account ID
from RESTlet raw/display values. It does not treat every Excel-compatible
`"44"` as blank. It queries active `Account.Account_ID__c` through CRM API and
admits only exactly one match, writing the matching `Account.Id` as
`CRM_Account__c`. Blank, zero-match, and multi-match rows are put only into
the restricted rejection ledger. The accounting invariant is:

```text
source_total = admitted_exact_match + excluded_blank_account_id
             + excluded_missing_account + excluded_ambiguous_account
```

## Describe, commit, and readback gates

Default dry-run performs no NetSuite or CRM authentication. `--validate-only`
is the separately authorized read-only API mode: it binds expected project,
environment, base URL, and API version; authenticates the machine client;
hashes the live Describe response; validates the 51-field object contract;
and performs Account admission without a write.

Before `--commit`, all of these must be supplied together: expected target base
URL/API version, expected project/environment, expected Describe SHA-256,
frozen candidate run ID, and frozen candidate manifest SHA-256. Describe must
confirm the target object fields, required `CRM_Account__c`, required source
Account ID, required identity/lifecycle controls, and unique/createable/
updateable `NetSuite_Source_Key__c`.

The writer sends at most 200 records per request to:

```text
PATCH /services/data/v63.0/composite/sobjects/
NetSuite_Revenue_Voucher_Line__c/NetSuite_Source_Key__c
```

New records include the required 51-field values, including `Name`, first/last
seen controls, `Is_Source_Active__c=true`, and a non-null exact Account
relation. Existing unchanged hashes only receive last-seen controls; first-seen
controls and CRM system fields are absent from update payloads. Every API
result is retained in the restricted attempt ledger. A failed batch stops the
run and cannot trigger soft deactivation; v1 has soft deactivation disabled.

After all batches succeed, the API queries the admitted external keys and
checks active count, source-key/hash, transaction/base amount totals,
`CRM_Account__c`, and non-null Account relation parity. Console output is
limited to identities, counts, hashes, and plan metadata; customer rows and
credentials remain outside logs.

## Local acceptance

This fixture creates the verified 1,789-row/35-field candidate with no external
connection:

```bash
uv run pingcap-pipeline netsuite-revenue-to-nexuscrm \
  --posting-period-id 245 \
  --run-id local-workbook-245 \
  --fixture-workbook "/Users/amber/Downloads/2607 revenue voucher list.xlsx"
```

The workbook intentionally lacks RESTlet raw/display evidence and therefore
cannot be admitted or committed. Local redacted RESTlet-envelope and CRM-client
tests cover real literal `44`, blank/missing/ambiguous Account rejection,
Describe drift, API batch/result behavior, payload whitelisting, replay, and
zero-write dry-run. A separate future Staging API acceptance must bind the
runtime, machine-user scope, Describe hash, upsert/readback behavior, retry,
and no-deactivation guarantee before any Production API authorization.
