# Order sync migration: Salesforce → NexusCRM

Date: 2026-06-30

Last amended: 2026-08-03. The Order child contract is now the standard
`OrderItem` object. The earlier custom `Order_Product__c` writer is superseded
and must not be used for new data-trans orders. RevRec and Collection now keep
their local completion lifecycle while skipping only external Salesforce
writes.

## Context

data-trans pushes generated CRM data (order / revrec / collection) to a
Salesforce-compatible API, configured via `sf.*` viper keys. NexusCRM exposes
the **same** SF-compatible API: `/services/oauth2/token` (password grant) and
`/services/data/vXX/sobjects/{Object}`.

Verified against NexusCRM staging:

- data-trans's exact order payload (v59.0, no `OpportunityId` / `OwnerId` /
  `Name`, with `Pricebook2Id` / `Product_Family__c` /
  `Accumulate_revenue_to_Org__c`) creates an Order successfully.
- Account ids match — NexusCRM preserved the original SF account ids
  (e.g. `001BV000004IQ7XYAW` HoYoverse), so `order.AccountId` resolves.
- Those accounts carry `BillingCountryCode`, which the Order `OrderNumber`
  autonumber requires.

## Goal

- Order create/update plus its standard OrderItem → **NexusCRM** (staging
  first).
- RevRec and Collection external Salesforce create/update calls → **disabled**.
  Their local parent-Order resolution, invoice-file enrichment, business
  fields, and completion-state persistence remain active.

## Design

### A. Order → NexusCRM

1. New `nexuscrm.*` viper namespace mirroring `sf.*`: `tokenDomain`, `domain`,
   `clientId`, `clientSecret`, `username`, `password`. Supplied via
   `config.yaml` (local `./config`, prod GCP secret). `sf.*` is left untouched.
2. `token_center.go`: add `fetchNexusCRMToken` (reads `nexuscrm.*`); add
   `case "nexuscrm"` to `fetchNewToken`.
3. `salesforce.go` request layer: `sendRequest` / `getInfoById` / `upSfFields`
   gain a `target` argument (`"salesforce"` default | `"nexuscrm"`) that
   selects domain + token. All existing callers keep the salesforce default
   (behaviour unchanged).
4. Order path targets NexusCRM: `createOrder`, `updateOrder`,
   `ensureOrderItem`, and their internal read/update calls.
5. New Orders explicitly use `CurrencyIsoCode=USD`, matching the upstream
   `order_total_fees_usd` authority. `ensureOrderItem` reads back the Order's
   Pricebook and currency and resolves exactly one active `PricebookEntry`
   scoped to that Pricebook, Product, and currency, then queries by
   `(OrderId, PricebookEntryId)` before creating. A matching row is reused; a
   duplicate or field mismatch blocks the sync.
6. The standard OrderItem payload is `OrderId`, `PricebookEntryId`,
   `Quantity=1`, `UnitPrice`, `ServiceDate`, `EndDate`, `Net_fees__c`, and
   `Account__c`. It does not write `OrderProduct__c`, `Order__c`, or the custom
   `Order_Product__c` object.
7. A newly created Order identity is persisted with `sync=0`. The row advances
   to `sync=1` only after OrderItem create/readback (or exact reuse) and Order
   activation both succeed. This makes an OrderItem or activation failure
   retryable without creating another Order.

### B. Skip external Salesforce writes; keep local completion

8. `SyncSalesforce` keeps its RevRec and Collection loops active, but routes
   each row only through local completion helpers. It never calls the legacy
   Salesforce create/update methods for these entity types.
9. `SalesforceLastInvoiceMonth` and `SyncSingleSF` retain functional RevRec and
   Collection cases. They run the same local completion helpers rather than
   returning an early no-op.
10. RevRec local completion preserves all business fields and any existing
    `RevrecID` / `RevrecNumber`, resolves the NexusCRM parent Order when the
    legacy create path would have done so, then persists `sync=1` and
    `sync_time`. It never fabricates a Revenue identity.
11. Collection local completion preserves all business fields and any existing
    `CollectionID` / `CollectionNumber`, resolves the NexusCRM parent Order for
    incomplete non-self-serve rows, downloads `Files` into base64
    `FileContent` when needed, then persists `sync=1` and `sync_time`. It does
    not create `Invoice_Collection__c` and does not PATCH Order
    `Is_Stripe__c`.
12. For RevRec and Collection, `sync=1` now means the required local completion
    contract succeeded; it is not evidence that a Salesforce object exists.
    A parent-resolution, file-download, or database-update error leaves the row
    retryable and fails the batch instead of being swallowed.
13. Legacy `createRevrec` / `updateRevrec` / `createCollection` implementations
    remain isolated for migration archaeology, but active batch and single-row
    routes do not call them. `Fix`, `FixUp`, and `EventCenter` remain disabled.

### C. Keep

Request-layer mechanics (reused by the order path) and the NexusCRM token.
`fetchSalesforceToken` is retained but unused by the order path.

## Config (add to config.yaml — values from secret, never committed)

```yaml
nexuscrm:
  base_url: https://nexuscrm-new-staging.pingcap.com
  api_version: v63.0
  client_id: <connected-app-client-id>
  client_secret_env: NEXUSCRM_FINANCE_ORDER_BACKFILL_CLIENT_SECRET
  scope: api
```

## Testing

- Unit: cover token switch, request target, OrderItem payload, exact
  readback, idempotent reuse, duplicate/mismatch rejection, and retryable
  `sync=0` state.
- Staging: create a disposable Draft Order and one standard OrderItem, read
  both back, prove no `Order_Product__c` row was created, retry the ensure
  lookup without a second child, then delete child before parent and read back
  the cleanup.

### Staging acceptance evidence

PASS at `2026-08-03T07:33:47Z` against NexusCRM Staging v63.0, SSM command
`21b4605e-f75f-40c7-bc49-8c79be084e13`:

- Created Draft Order `SG-OF-26080301` and one standard OrderItem with the
  unique active USD PricebookEntry `01u5g00000196DEAAY`.
- Direct and SOQL readback matched the required standard fields; retry lookup
  returned exactly one row and performed no second POST.
- `Order_Product__c` count for the disposable Order was zero.
- Deleted the OrderItem and then the Draft Order; independent SOQL readback
  returned neither record.

## Risks

- Order `OrderNumber` autonumber requires the account's country code (present
  for migrated accounts).
- The configured Product must have exactly one active USD PricebookEntry in
  the configured Pricebook. Zero or multiple scoped matches fail closed; an
  Order with a different Pricebook or currency is also rejected.
- NexusCRM has no unique constraint on `(OrderId, PricebookEntryId)`; the
  service serializes single-order work locally and independently rejects a
  duplicate during post-create readback.
- revrec / collection no longer create or update Salesforce objects. Their
  local completion contract remains active and must not be interpreted as
  proof that an external CRM object exists.
