Skip to content

feat(salesforce): initial scaffold port from deco-cx/apps#71

Merged
JonasJesus42 merged 1 commit into
mainfrom
JonasJesus42/salesforce-personalization-port
Jun 3, 2026
Merged

feat(salesforce): initial scaffold port from deco-cx/apps#71
JonasJesus42 merged 1 commit into
mainfrom
JonasJesus42/salesforce-personalization-port

Conversation

@JonasJesus42

@JonasJesus42 JonasJesus42 commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Ports the Salesforce Marketing Cloud Personalization (Evergage) campaign API as a new @decocms/apps/salesforce package, mirroring the algolia scaffold pattern (#66).

Scope: read-path loaders only — homepage shelves, PDP recommendations, cart-aware cross-sell. No actions, no analytics sections. Follow-up PRs will add coverage in line with the algolia series (#67/#68/#69).

Layout

salesforce/
├── README.md
├── index.ts                       # type/util re-exports
├── types.ts                       # SalesforceProduct (open via index sig), body/response shapes
├── utils/
│   ├── parseUserCookie.ts         # puid → encryptedId, uuid → anonymousId, anonymous fallback
│   ├── httpClient.ts              # runtime-agnostic Proxy client (CF Workers + Bun + Node)
│   └── transform.ts               # createProductTransformer({ propertyMapper? })
├── loaders/products/
│   ├── list.ts                    # homepage campaign shelf
│   ├── listRecomended.ts          # PDP related-products
│   └── listCart.ts                # cart cross-sell (Replace Cart)
└── __tests__/                     # 48 tests, fetch mocked
    ├── parseUserCookie.test.ts
    ├── transform.test.ts
    └── httpClient.test.ts

Key design choices vs algolia

  • No configureSalesforce global — every loader takes baseUrl / dataset / campaignId / cookieName via props. Real sites run multiple Evergage datasets per worker (homepage uses dataset A, PDP uses dataset B), so a single module-global config doesn't fit.
  • Cookie reading via getCookies() from @tanstack/react-start/server (parameterless, ALS-backed) instead of getCookies(req.headers). The framework's commerceLoader(resolvedProps) path drops the req argument before reaching the loader, so the deferred-section call sites can't read cookies through the explicit-headers approach magento uses. The dynamic import is wrapped in try/catch so unit tests don't blow up outside a TanStack request boundary.
  • propertyMapper hook on the transformer — Evergage datasets expose dataset-specific columns (Marca, Volume, Linha, tag__phebo, freeShipping, …). The default mapper outputs only the always-present fields (itemType, category); site-side wrappers pass a mapper to project their custom columns into schema.org/PropertyValue[] without forking the schema.org map. SalesforceProduct keeps an [customField: string]: unknown index signature so the mapper can read raw extras safely.

Error handling

Each loader wraps its POST in try/catch and logs to console.error("[salesforce/products/...] failed:", err.message) before returning null. The legacy Deno loaders returned silent null on error — we keep the same return shape (callers don't need to handle rejections) but surface the error so API outages and CORS regressions don't hide behind an empty shelf during parity validation.

Package wiring

  • package.json — adds ./salesforce, ./salesforce/types, ./salesforce/utils/*, ./salesforce/loaders/products/* exports + salesforce/ to files + @tanstack/react-start >=1 to peerDependencies (transitively present via @decocms/start, now explicit so knip stays happy).
  • biome.json — includes salesforce/**.
  • knip.json — adds entry globs.

Test plan

  • bun run typecheck clean
  • bun run test — 49 files / 595 tests pass (48 new under salesforce/__tests__/)
  • bun run lint on salesforce/ — clean except 1 noExplicitAny warning on the Proxy return type (matches the same suppression already in magento/client.ts:112)
  • bun run lint:unused — no salesforce-specific complaints
  • Downstream validation in granadobr-tanstack once published (Fase B of plan)

🤖 Generated with Claude Code


Summary by cubic

Add @decocms/apps/salesforce, a port of the Evergage campaign API for homepage shelves, PDP recommendations, and cart cross-sell. Loaders are stateless, read-only, and include a flexible propertyMapper transformer hook.

  • New Features

    • Three loaders: salesforce/loaders/products/list.ts, listRecomended.ts, and listCart.ts.
    • Stateless config via props (baseUrl, dataset, campaignId, cookieName); no global configureSalesforce.
    • Cookie parsing via getCookies() from @tanstack/react-start/server with safe anonymous fallback (utils/parseUserCookie.ts).
    • Runtime-agnostic HTTP client with indexed-route syntax (utils/httpClient.ts).
    • Product transformer factory with createProductTransformer({ propertyMapper }) (utils/transform.ts).
    • Errors are logged and return null to match existing call sites.
    • 48 tests covering cookie parsing, HTTP client, and transformer.
  • Dependencies

    • Added exports and files entry for ./salesforce/* in package.json.
    • New peer dependency: @tanstack/react-start >=1.
    • Updated biome.json and knip.json globs to include salesforce/**.

Written for commit e9f8e94. Summary will update on new commits.

Review in cubic

Ports the Salesforce Marketing Cloud Personalization (Evergage)
campaign personalization API as a new `@decocms/apps/salesforce`
package, mirroring the algolia scaffold pattern (#66).

Scope: read-path loaders for homepage shelves, PDP recommendations,
and cart-aware cross-sell. No actions, no analytics. The legacy
Deno loaders are stateless — config (`baseUrl` / `dataset` /
`campaignId` / `cookieName`) comes in via loader props rather than a
global `configureSalesforce`, since real sites typically run multiple
Evergage datasets per worker.

Layout:
- `types.ts` — `SalesforceProduct` (open via index signature),
  `PersonalizationBody`, response shapes.
- `utils/parseUserCookie.ts` — decode the URL-encoded JSON cookie
  Evergage drops on the browser; falls back to anonymous fallback.
- `utils/httpClient.ts` — runtime-agnostic Proxy client supporting
  the legacy `client["POST /api2/event/:dataset"]` indexed-route
  syntax used by every Deno-era loader.
- `utils/transform.ts` — `createProductTransformer({ propertyMapper? })`
  so site-side wrappers can project dataset-specific Evergage columns
  (`Marca`, `Volume`, `Linha`, …) without forking the schema.org map.
- `loaders/products/{list,listRecomended,listCart}.ts` — the three
  campaign endpoints granadobr-tanstack consumes today.

Cookie access goes through `getCookies()` from
`@tanstack/react-start/server` (parameterless, ALS-backed) because
the framework's `commerceLoader(resolvedProps)` path drops the
`req` argument before reaching the loader. Mirrors the workaround
already used in helsinki's site-side packs.

Tests: 48 new tests (parseUserCookie, transform, httpClient) lock
the contract against the deco-cx/apps Deno baseline — all mock
fetch, no Evergage credentials needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@JonasJesus42 JonasJesus42 requested a review from a team June 3, 2026 06:33
@JonasJesus42 JonasJesus42 merged commit d353aa9 into main Jun 3, 2026
1 of 2 checks passed
@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 3.1.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant