Salesforce Integration Guide

How the NestJS html-parser service receives cart submissions from the embedded quote UI, authenticates with Salesforce OAuth, and creates quotes via Apex REST /services/apexrest/Quote/.

1. Overview

On cart submit, the API validates the payload, maps frontend dates to the Apex contract, exchanges OAuth credentials for an access_token and instance_url, posts the quote to Salesforce, and returns { success, message } to the browser.

  1. Validate payload (CreateCartDto / CartItemDto)
  2. Map cart[].DATEcart[].DATA
  3. Obtain OAuth token via SALES_FORCE_AUTH_URL / SALES_FORCE_GRANT_TYPE
  4. Send POST {instance_url}/services/apexrest/Quote/
  5. Proxy Apex { success, message } to the client
Pre go-live gate: Product2 TDS ID Apex expects cart[].ID to be the TDS ID of a Salesforce Product2 record. The embedded frontend prefers data-tds-id on BOM rows; otherwise it falls back to PARTNUMBER + PARENT, which is not a valid Product2 Id and will fail product lookup until the export exposes real TDS IDs.

2. Architecture

The integration is implemented by POST /api/cart (CartController), OAuth in SalesforceTokenService, and Apex submission in CartService.

flowchart LR
  Browser["Browser / scripts.js"]
  CartEndpoint["POST /api/cart"]
  CartController["CartController"]
  Mapper["DATE to DATA mapper"]
  CartService["CartService"]
  Token["SalesforceTokenService"]
  Apex["Apex REST /Quote/"]
  SF[("Salesforce CRM")]

  Browser --> CartEndpoint
  CartEndpoint --> CartController
  CartController --> Mapper
  Mapper --> CartService
  CartService --> Token
  Token --> CartService
  CartService --> Apex
  Apex --> SF
  SF --> Apex
  Apex --> CartService
  CartService --> CartController
  CartController --> Browser
            

3. End-to-end journey

Happy path from the embedded cart UI through validation, mapping, OAuth, Apex Quote creation, and the JSON response shown to the user. On failure after mapping, an asynchronous SMTP alert may also be sent.

sequenceDiagram
  participant UI as scripts.js
  participant API as CartController
  participant Map as QuoteMapper
  participant Svc as CartService
  participant OAuth as TokenService
  participant Apex as ApexREST
  participant Mail as ErrorMail

  UI->>API: POST /api/cart
  API->>API: Validate DTO
  API->>Map: Map DATE to DATA
  Map->>Svc: QuotePayload
  Svc->>OAuth: refresh token
  OAuth-->>Svc: access_token + instance_url
  Svc->>Apex: POST /services/apexrest/Quote/
  alt success
    Apex-->>Svc: 200 success true
    Svc-->>API: Quote result
    API-->>UI: success message
  else business or transport error
    Apex-->>Svc: 400 / 5xx / network
    Svc-->>API: Mapped error
    API-->>UI: success false + message
    API->>Mail: Async failure alert
  end
            

4. Authentication

Each cart submission performs a fresh OAuth exchange (no in-memory token cache). The token URL and grant type come from environment variables; SALES_FORCE_ENV is only a label for logs and e-mails and does not select the OAuth host.

Variable Required Description
SALES_FORCE_AUTH_URL Yes Full OAuth token URL (typically /services/oauth2/token)
SALES_FORCE_GRANT_TYPE Yes client_credentials or refresh_token
SALES_FORCE_CLIENT_ID Yes Connected App client ID
SALES_FORCE_CLIENT_SECRET Yes Connected App client secret
SALES_FORCE_REFRESH_TOKEN If refresh_token Required only when grant type is refresh_token
SALES_FORCE_ENV Yes Label for logs/e-mails (sandbox / production)
SALES_FORCE_INSTANCE_URL No Dev override only; forbidden when NODE_ENV=production
SALES_FORCE_QUOTE_PATH No Default /services/apexrest/Quote/

OAuth request

POST {SALES_FORCE_AUTH_URL} with application/x-www-form-urlencoded body: grant_type, client_id, client_secret, and refresh_token when applicable. Response fields used: access_token, instance_url.

Deprecated (unused by cart module): SALES_FORCE_URL_TOKEN, SALES_FORCE_USERNAME, SALES_FORCE_PASSWORD (legacy password grant).

5. API contracts

Public endpoint

  • Method: POST
  • Path: /api/cart
  • Handler: CartController.create()
  • Validation: global/controller ValidationPipe with whitelist + forbid non-whitelisted

Request payload

{
  "cnpj": "12345678000199",
  "company": "Example Corp",
  "PV": "PV123",
  "PVZ": "PVZ456",
  "email": "user@example.com",
  "user": "jdoe",
  "language": "pt",
  "defaultDate": "2026-06-05",
  "cart": [
    {
      "ID": "01tXXXXXXXXXXXXXXX",
      "QTY": "2",
      "PARTNUMBER": "MAT-001",
      "DATE": "2026-06-05"
    }
  ]
}
Field Required Notes
cnpj Yes Account lookup in Apex
company Yes Opportunity / quote naming source
cart Yes (non-empty) Line items
cart[].ID Yes Product2 TDS ID
cart[].QTY Yes Quantity
cart[].DATE No Mapped to DATA server-side
cart[].DATA No Sent as-is when provided
Other cart fields No PARENT, ITEM, PARTNUMBER, DESCR, INFO, COUNT

Success response (HTTP 200)

{
  "success": true,
  "message": "Quote created successfully"
}

Business error (HTTP 400 from Salesforce)

{
  "success": false,
  "message": "Product not found"
}

Apex REST

POST {instance_url}{SALES_FORCE_QUOTE_PATH} with Authorization: Bearer {access_token}. Apex resolves Account by cnpj and Product2 by cart[].ID (TDS ID), and creates Opportunity + Quote + QuoteLineItem server-side.

6. Payload mapping

The mapper builds the Apex body from the validated DTO. Item dates prefer DATA, then DATE, then root defaultDate. Empty or null dates omit DATA.

flowchart TB
  subgraph Frontend["Frontend scripts.js"]
    TDS["data-tds-id preferred"]
    Fallback["else PARTNUMBER + PARENT"]
    DateField["DATE on cart item"]
    DefaultDate["defaultDate query/root"]
  end

  subgraph API["html-parser"]
    ResolveID["cart[].ID"]
    MapDate["DATE to DATA"]
    Payload["QuotePayload"]
  end

  subgraph Apex["Salesforce Apex"]
    Account["Account by cnpj"]
    Product["Product2 by TDS ID"]
    Quote["Opportunity + Quote + QLI"]
  end

  TDS --> ResolveID
  Fallback --> ResolveID
  DateField --> MapDate
  DefaultDate --> MapDate
  ResolveID --> Payload
  MapDate --> Payload
  Payload --> Account
  Payload --> Product
  Account --> Quote
  Product --> Quote
            
Source Apex field
cart[].DATE cart[].DATA
cart[].DATA Preferred when already present
root defaultDate Fallback when item has neither DATE nor DATA
ID, QTY Always sent
PARENT, ITEM, PARTNUMBER, … Included only when defined

Frontend ID resolution

  1. Prefer row.dataset.tdsId (data-tds-id)
  2. Else PARTNUMBER + PARENT (composite key — go-live risk)

Submit URL: POST {HOST}/api/cart with cart plus query-derived PVZ, PV, cnpj, email, company, user, language, and defaultDate. UI treats data.success === true as success and shows data.message on error.

7. Error handling & retries

flowchart TD
  Start["Cart submit"] --> Validate["Validate DTO"]
  Validate -->|invalid| Client400["HTTP 400 validation"]
  Validate -->|ok| OAuth["OAuth token exchange"]
  OAuth -->|transient 502/503/504 or network| OAuthRetry["Retry up to 3"]
  OAuthRetry --> OAuth
  OAuth -->|client error| Auth500["500 Unable to authenticate"]
  OAuth -->|ok| Quote["POST Apex Quote"]
  Quote -->|401 or 403| Reauth["Fresh OAuth + retry up to 3"]
  Reauth --> Quote
  Quote -->|400| Biz400["400 success false + message"]
  Quote -->|network| Svc503["503 Unable to reach Salesforce"]
  Quote -->|timeout| Svc504["504 timed out"]
  Quote -->|5xx| Svc500["500 Salesforce service error"]
  Quote -->|200| Ok["200 success true"]
  Auth500 --> Mail["Async e-mail alert"]
  Biz400 --> Mail
  Svc503 --> Mail
  Svc504 --> Mail
  Svc500 --> Mail
            
Condition HTTP status Body
Token / OAuth failure 500 { success: false, message: "Unable to authenticate with Salesforce" }
401/403 after retries 503 { success: false, message: "Salesforce authentication failed" }
Network / unreachable 503 { success: false, message: "Unable to reach Salesforce" }
Timeout 504 { success: false, message: "Salesforce request timed out" }
Salesforce 5xx 500 { success: false, message: "Salesforce service error" }
Apex business error 400 { success: false, message: "<Apex message>" }
  • OAuth: up to 3 automatic retries on network errors or HTTP 502/503/504. Client errors such as invalid_grant are not retried.
  • Apex REST: on HTTP 401 or 403, a new OAuth exchange runs and the quote request is retried up to 3 times (SALESFORCE_MAX_RETRIES).
  • DTO validation failures do not trigger e-mail alerts.

8. Observability

Every cart submission emits structured Winston logs correlated by requestId and flow: "cart". Logs go to the console and to logs/combined-*.log (hourly rotation, 14-day retention).

Step When Notable fields
cart.start Request received itemCount, PV/PVZ fingerprints
cart.map DTO mapped Full Apex payload
sf.oauth.request Token call authUrl, grantType, redacted curl
sf.oauth.response Token obtained instanceUrl
sf.quote.request Apex POST url, payload, redacted curl
sf.quote.response Apex success status, success, message
sf.quote.error Failure status, errorCode, responseBody, curl
cart.complete / cart.error Terminal Result or errorType

cURL reproduction

Outbound Salesforce calls include a curl field with secrets redacted ((***REDACTED***). To reproduce a sandbox failure: find the requestId, copy the cURL from sf.quote.request or sf.oauth.request, replace the redacted bearer token with a fresh access_token, and run locally. Never commit secrets.

E-mail alerts

After payload mapping, failures (Salesforce 400, auth, network, timeout, 5xx) trigger an asynchronous internal e-mail via QuotationErrorNotificationService / MailService. Content includes requestId, timestamp, SALES_FORCE_ENV, error summary, and the full Apex JSON payload. Disable with SMTP_ENABLED=false.

Variable Default Description
SMTP_ENABLED true Set false to disable alerts
SMTP_HOST / SMTP_PORT relay / 25 Internal SMTP
SMTP_TO ops mailboxes Comma-separated recipients
SMTP_FROM_* Portal B2C From display name and address
Logging policy: never log OAuth client_secret, refresh_token, or raw access_token. PV/PVZ are fingerprinted at cart.start. cnpj, company, and email appear in Apex payload logs and alert e-mails by design for troubleshooting — restrict access in production.

9. Go-live gates / UAT notes

Blocking: Product2 TDS ID Composite IDs such as PARTNUMBER + PARENT are not Salesforce Product2 Ids and are not looked up as DWCodigoExterno__c. Export must provide data-tds-id (or equivalent) before production cutover.

Known Salesforce UAT issues

  • HTTP 500 — getPricebookByAccountId: Account has no Pricebook; Apex throws QueryException instead of structured 400. Fix data and harden Apex.
  • HTTP 500 — account not found: unknown CNPJ can surface as NullPointerException. Apex should return structured 400.

Manual QA checklist (sandbox)

  • Happy path (200) — valid cnpj, company, and known Product2 TDS IDs → { success: true }
  • Product not found (400) — unknown cart[].ID{ success: false, message: "..." }
  • Account not found (400) — unknown cnpj → structured business error (after Apex hardening)

10. Related source files

File Role
src/cart/cart.controller.ts Public POST /api/cart, validation, error mapping, e-mail trigger
src/cart/cart.service.ts Apex REST client + 401/403 retry
src/cart/salesforce/salesforce-token.service.ts OAuth via auth URL + grant type
src/cart/salesforce/salesforce-quote.mapper.ts DTO → Apex payload (DATEDATA)
src/cart/salesforce/salesforce-error.util.ts Normalize Apex/SF error bodies for logs
src/cart/dto/create-cart.dto.ts / cart-item.dto.ts Request validation
src/cart/quotation-error-notification.service.ts Failure e-mail content
src/mail/mail.service.ts SMTP transport
assets/scripts.js Embedded cart UI, TDS ID resolve, submit
SALESFORCE_INTEGRATION.md Canonical markdown guide in the repository