Skip to content

Add "Sync Ortto contact" event for v6 contact sync (giveth-v6-core#426) - #136

Open
ae2079 wants to merge 6 commits into
stagingfrom
feat/426-sync-ortto-contact
Open

Add "Sync Ortto contact" event for v6 contact sync (giveth-v6-core#426)#136
ae2079 wants to merge 6 commits into
stagingfrom
feat/426-sync-ortto-contact

Conversation

@ae2079

@ae2079 ae2079 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Companion notification-center change for Giveth/giveth-v6-core#426 (Sync v6 users to Ortto as contacts by canonical email). Adds a dedicated event that upserts an Ortto contact without sending any email. Pairs with giveth-v6-core PR #441 — deploy this first so the new NotificationType is seeded before v6-core starts emitting the event (otherwise v6's requests 400 with INVALID_NOTIFICATION_TYPE).

Changes

  • New Sync Ortto contact event (NOTIFICATIONS_EVENT_NAMES / NOTIFICATION_TYPE_NAMES), ORTTO category, givethio microservice, with a seed migration for the NotificationType.
  • activityCreator case that, for this event, always merges the Ortto person on the stable str:cm:v6-user-id (regardless of ENVIRONMENT) so a canonical-email change re-points the same contact instead of creating a duplicate, and stamps a durable bol:cm:sourced-from-v6 person-field marker so v6-managed contacts stay distinguishable from legacy v5 ones. Uses a dedicated inert activity act:cm:sync-ortto-contact (not created-profile) so the sync can never re-fire a welcome journey.
  • syncOrttoContact segment validator (email, userId required; firstName/lastName optional/blank) so nameless wallet/Turnkey profiles still sync.
  • callOrttoActivity now returns a boolean success (logs, still doesn't throw); for the Sync Ortto contact event, sendNotification returns a 502 when the Ortto upsert fails, so v6-core records a false success only when the upsert is confirmed and its reconcile cron retries otherwise. All other Ortto events keep their existing fire-and-forget behavior.

Deploy prerequisite (Ortto workspace)

The Ortto workspace must define custom fields str:cm:v6-user-id and bol:cm:sourced-from-v6, and the activity sync-ortto-contact with no automation/journey bound to it.

How to Test

  1. Run migrations (yarn db:migrate:run:local) so the Sync Ortto contact type is seeded; set EMAIL_ADAPTER=mock.
  2. POST /v1/thirdParty/notifications (Basic auth, givethio) with { eventName: "Sync Ortto contact", segment: { payload: { email, userId } } }.
  3. Confirm the resulting Ortto activity payload merges on str:cm:v6-user-id, sets bol:cm:sourced-from-v6: true, uses act:cm:sync-ortto-contact, and that no email is sent.
  4. Confirm a failing Ortto call surfaces as a 502 for this event (and stays fire-and-forget for other events).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added Ortto contact synchronization using email, user identity, and optional name details.
    • Added validation requiring a valid email address and positive user ID.
    • Contact updates use a stable identity to help prevent duplicates.
    • Added configurable request timeouts, defaulting to 10 seconds.
  • Bug Fixes

    • Synchronization failures are now clearly reported instead of recorded as successful.
    • Improved success and failure reporting, including retry handling for temporary failures.

ae2079 and others added 2 commits July 29, 2026 20:07
New ORTTO-category NotificationType (givethio) that upserts an Ortto
person WITHOUT sending an email. Unlike "Create Ortto profile" it always
merges on the stable v6 user id (str:cm:v6-user-id) regardless of
environment, so a canonical-email change re-points the same contact
instead of creating a duplicate, and it stamps a durable
bol:cm:sourced-from-v6 marker so v6-managed contacts are distinguishable
from legacy v5-sourced ones.

- notifications types: SYNC_ORTTO_CONTACT event + reuse the existing
  "created-profile" Ortto activity (no new Ortto activity required)
- general/notificationType: NOTIFICATION_TYPE_NAMES + schemaValidator
- segment validator syncOrttoContact { email, userId required; names
  optional/blank so wallet-only & Turnkey profiles still sync }
- activityCreator case + always-merge-by-v6-user-id + marker
- seed migration for the new NotificationType

Requires the Ortto workspace to define custom fields str:cm:v6-user-id
and bol:cm:sourced-from-v6.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…(#426)

Adversarial-review fixes for the Sync Ortto contact event:
- callOrttoActivity now returns a boolean success (logs, does not throw);
  sendNotification 502s the Sync Ortto contact event when the Ortto call
  fails, so v6-core sees a non-2xx and its reconcile cron retries instead of
  recording a false success. Other Ortto events are unchanged (fire-and-forget;
  the boolean is ignored).
- Point the sync at a DEDICATED inert Ortto activity (act:cm:sync-ortto-contact)
  instead of reusing act:cm:created-profile, so a contact sync (which fires
  again on every canonical-email re-point) can never re-trigger a created-profile
  welcome journey/email — the sync must be side-effect-free.

Ortto workspace prerequisites: activity `sync-ortto-contact` (no automation
bound) and custom fields `str:cm:v6-user-id`, `bol:cm:sourced-from-v6`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@ae2079, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 9 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c33a2440-91aa-460e-b278-b7e90154368d

📥 Commits

Reviewing files that changed from the base of the PR and between 9973664 and 2357755.

📒 Files selected for processing (2)
  • src/adapters/emailAdapter/orttoMockAdapter.ts
  • src/services/notificationService.ts

Walkthrough

Adds a Sync Ortto contact notification type, validates and normalizes its payload, constructs a v6 identity-based Ortto activity, and maps Ortto outcomes to explicit HTTP errors.

Changes

Ortto Contact Sync

Layer / File(s) Summary
Notification contracts and validation
src/types/general.ts, src/types/notifications.ts, src/entities/notificationType.ts, src/utils/validators/..., src/validators/schemaValidators.ts, migrations/...
Registers the notification name, Ortto event mapping, Joi validator, and database notification type.
Validated contact activity payload
src/services/notificationService.ts, src/services/notificationService.test.ts, src/utils/errorMessages.ts
Normalizes contact data, adds optional names, and builds a v6-user-id merge payload with a v6 source marker.
Structured Ortto result handling
src/adapters/emailAdapter/..., src/services/notificationService.ts, src/adapters/emailAdapter/orttoAdapter.test.ts, config/example.env
Returns structured success and failure results, applies a configurable timeout, classifies failures, and maps contact-sync failures to 422 or 502 errors.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant NotificationService
  participant JoiValidator
  participant OrttoAdapter
  participant Ortto

  Caller->>NotificationService: Submit Sync Ortto contact payload
  NotificationService->>JoiValidator: Validate and coerce payload
  JoiValidator-->>NotificationService: Return normalized contact data
  NotificationService->>OrttoAdapter: Send v6 identity merge activity
  OrttoAdapter->>Ortto: Submit activity with timeout
  Ortto-->>OrttoAdapter: Return success or failure
  OrttoAdapter-->>NotificationService: Return OrttoActivityResult
  NotificationService-->>Caller: Return success, 422, or 502
Loading

Suggested reviewers: ramramez

Poem

I’m a rabbit with contacts to sync,
Joi trims each field in a blink.
V6 IDs keep identity clear,
Ortto results now appear,
With 422 or 502 in the link.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the new Sync Ortto contact event and its v6 contact synchronization purpose.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/426-sync-ortto-contact

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/adapters/emailAdapter/orttoAdapter.ts (1)

7-37: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Prevent the Ortto upsert from blocking the sync response.

callOrttoActivity awaits axios.request(config) with no finite timeout, and sendNotification waits for this promise before it can throw the 502 used for contact-sync retries. Add a small, validated request timeout so a stalled Ortto connection fails back through the caller’s error path instead of keeping the request indefinitely pending.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/adapters/emailAdapter/orttoAdapter.ts` around lines 7 - 37, Update
callOrttoActivity’s axios request configuration to include a small finite
timeout, sourced from an existing configuration value or validated constant, so
stalled Ortto requests reject promptly and preserve sendNotification’s 502 retry
path.
🧹 Nitpick comments (1)
src/services/notificationService.ts (1)

57-64: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add regression coverage for the new cross-layer contract.

Test the exact activity ID, merge_by, v6 marker field, optional names, and both adapter outcomes: false must produce HTTP 502 only for SYNC_ORTTO_CONTACT, while existing Ortto events remain non-throwing.

Also applies to: 226-245, 359-377

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/notificationService.ts` around lines 57 - 64, Extend regression
tests around the SYNC_ORTTO_CONTACT handling to assert the exact activity ID,
merge_by value, v6 marker field, and support for omitted optional first and last
names. Cover both adapter outcomes: a false result returns HTTP 502 only for
SYNC_ORTTO_CONTACT, while existing Ortto event types remain non-throwing and
retain their current behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/adapters/emailAdapter/orttoAdapter.ts`:
- Around line 28-37: Update the error handling in orttoActivityCall so
logger.error does not include the full data payload or raw Axios error/config.
Log only a safe allowlist of non-sensitive identifiers and a sanitized error
summary, excluding email, names, v6-user-id, headers, and request configuration.

---

Outside diff comments:
In `@src/adapters/emailAdapter/orttoAdapter.ts`:
- Around line 7-37: Update callOrttoActivity’s axios request configuration to
include a small finite timeout, sourced from an existing configuration value or
validated constant, so stalled Ortto requests reject promptly and preserve
sendNotification’s 502 retry path.

---

Nitpick comments:
In `@src/services/notificationService.ts`:
- Around line 57-64: Extend regression tests around the SYNC_ORTTO_CONTACT
handling to assert the exact activity ID, merge_by value, v6 marker field, and
support for omitted optional first and last names. Cover both adapter outcomes:
a false result returns HTTP 502 only for SYNC_ORTTO_CONTACT, while existing
Ortto event types remain non-throwing and retain their current behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6e386be8-3da2-4edd-b689-d632eb18c574

📥 Commits

Reviewing files that changed from the base of the PR and between 55f47c8 and 988f361.

📒 Files selected for processing (10)
  • migrations/1732000000000-seedNotificationTypeSyncOrttoContact.ts
  • src/adapters/emailAdapter/orttoAdapter.ts
  • src/adapters/emailAdapter/orttoAdapterInterface.ts
  • src/adapters/emailAdapter/orttoMockAdapter.ts
  • src/entities/notificationType.ts
  • src/services/notificationService.ts
  • src/types/general.ts
  • src/types/notifications.ts
  • src/utils/errorMessages.ts
  • src/utils/validators/segmentAndMetadataValidators.ts

Comment thread src/adapters/emailAdapter/orttoAdapter.ts Outdated
…136)

- orttoAdapter: never log the payload (contact email / names / v6-user-id)
  or the raw Axios error (its `config` carries the X-Api-Key header and body);
  log only a sanitized summary (microService, activity ids, HTTP status,
  error message).
- orttoAdapter: add a finite request timeout (ORTTO_REQUEST_TIMEOUT_MS, default
  10s, validated) so a stalled Ortto connection rejects promptly instead of
  keeping the request — and the contact-sync 502 retry path — pending forever.
- Document ORTTO_REQUEST_TIMEOUT_MS in config/example.env.
- Add activityCreator regression tests for SYNC_ORTTO_CONTACT: dedicated inert
  activity id, merge_by str:cm:v6-user-id (env-independent), sourced-from-v6
  marker, and optional-names omission.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/services/notificationService.test.ts`:
- Around line 112-129: Update the activity builder used by activityCreator for
SYNC_ORTTO_CONTACT so firstname and lastname attributes are added only when
values are supplied, rather than with undefined values. Extend the existing
nameless-profile test to assert that result.activities[0].attributes excludes
both str:cm:firstname and str:cm:lastname while preserving the existing
attributes.
- Around line 90-109: Update the test’s ENVIRONMENT cleanup around the
activityCreator scenario to track whether process.env.ENVIRONMENT existed before
setting it. In the finally block, restore the saved value when the variable
originally existed; otherwise delete process.env.ENVIRONMENT instead of
assigning the undefined value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d47a8b2f-5cf6-4fbd-a36b-70ebc6d44bc3

📥 Commits

Reviewing files that changed from the base of the PR and between 988f361 and 0a5ef63.

📒 Files selected for processing (3)
  • config/example.env
  • src/adapters/emailAdapter/orttoAdapter.ts
  • src/services/notificationService.test.ts

Comment thread src/services/notificationService.test.ts
Comment thread src/services/notificationService.test.ts
@ae2079

ae2079 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the CodeRabbit review in 0a5ef63:

  • orttoAdapter.ts — sensitive data in logs (major): the catch no longer logs data (contact email / names / v6-user-id) or the raw Axios error (its config carried the X-Api-Key header + request body). It now logs a sanitized summary only: microService, activity ids, HTTP status, and the error message.
  • orttoAdapter.ts — no request timeout (major): added a finite timeout from ORTTO_REQUEST_TIMEOUT_MS (default 10s, validated) so a stalled Ortto connection rejects promptly and the contact-sync 502 retry path isn't held open indefinitely. Documented the var in config/example.env.
  • notificationService.ts — regression coverage (nitpick): added activityCreator tests for SYNC_ORTTO_CONTACT asserting the dedicated inert activity id (act:cm:sync-ortto-contact), merge_by: ['str:cm:v6-user-id'] regardless of ENVIRONMENT, the bol:cm:sourced-from-v6 marker, and optional-name omission. The false → 502 only for SYNC_ORTTO_CONTACT behavior is additionally covered consumer-side in giveth-v6-core (syncOrttoContact returns false on non-2xx → marker not advanced → cron retries).

Typecheck clean; the new test assertions validated.

…cleanup

- activityCreator (SYNC_ORTTO_CONTACT): include str:cm:firstname / str:cm:lastname
  only when supplied, so a nameless profile never sends `undefined` attributes.
- notificationService.test: assert the nameless activity omits both name
  attributes; restore process.env.ENVIRONMENT by delete-when-originally-unset
  instead of assigning undefined (which would leave the string "undefined").

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ae2079

ae2079 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the re-review in d3dc744:

  • Omit undefined name attributes: activityCreator for SYNC_ORTTO_CONTACT now adds str:cm:firstname / str:cm:lastname only when supplied, so a nameless wallet/Turnkey profile never sends undefined attributes to Ortto. Extended the nameless test to assert both are absent.
  • Safe ENVIRONMENT cleanup in the test: the finally now deletes process.env.ENVIRONMENT when it was originally unset (instead of assigning undefined, which would leave the string "undefined"), and restores the prior value otherwise.

Typecheck clean; assertions validated.

@ae2079
ae2079 requested a review from RamRamez August 4, 2026 01:27
@RamRamez

RamRamez commented Aug 4, 2026

Copy link
Copy Markdown
Member

Code review — P0–P2

Reviewed at extra-high effort. Build is clean (tsc --noEmit) and all 4 tests in src/services/notificationService.test.ts pass on this branch. Findings below are ordered by priority; P3 cleanups (backdated migration timestamp, hand-typed validator map key, duplicated event-name literal, timeout lower bound) are omitted.


🔴 P0

1. Every Ortto failure maps to a retryable 502, so a permanent 4xx is retried forever
src/services/notificationService.ts:376 · src/adapters/emailAdapter/orttoAdapter.ts:41

callOrttoActivity collapses all failures into one boolean, so "Ortto rejected the payload with 400" is indistinguishable from "Ortto is down / timed out". Both become a 502.

This matters because of the deploy ordering the PR itself describes: the code ships before the manual Ortto workspace edit. During that window Ortto returns 400 on every request — the activity sync-ortto-contact doesn't exist, str:cm:v6-user-id / bol:cm:sourced-from-v6 aren't defined, and (undocumented in the prerequisite) the activity must also declare the attributes str:cm:email, str:cm:v6-user-id, str:cm:firstname, str:cm:lastname that activityCreator sends. v6-core then never advances its per-user sync marker, and its reconcile cron re-attempts every user on every tick, indefinitely, with no path to convergence.

Suggested fix: branch on e.response?.status — surface 4xx as a non-retryable status (422/400) and reserve 502 for 5xx/timeout/network.


🟠 P1

2. Joi's coerced value is discarded, so a string userId becomes the raw Ortto merge key
src/utils/validators/segmentAndMetadataValidators.ts:166

validateWithJoiSchema only inspects validationResult.error and throws away validationResult.value, and sendNotification passes the original body.segment.payload to activityCreator. So payload.userId?.toString() stringifies the raw input, not the number Joi parsed.

Verified against joi 17.6:

input userId Joi error Joi's coerced value value actually sent as str:cm:v6-user-id
' 42 ' none 42 ' 42 '
'042' none 42 '042'
'4e1' none 40 '4e1'

Three distinct merge_by: ['str:cm:v6-user-id'] values for one v6 user → three Ortto contacts. That is precisely the duplication this PR exists to prevent. Joi.number() also accepts -1.5, which would become a merge key verbatim.

Suggested fix: Joi.number().integer().positive().required(), and/or have validateWithJoiSchema return validationResult.value for the caller to use.

3. A request with no segment returns 500, not 400 — and gets retried forever
src/services/notificationService.ts:358

Joi treats undefined as valid for a non-required object schema, so the new validator does not catch a missing segment. Verified: Joi.object({email: required, userId: required}).validate(undefined) returns {} — no error. Execution then reaches activityCreator(undefined, …), which throws TypeError: Cannot read properties of undefined (reading 'email') (reproduced directly). It isn't a StandardError, so errorHandler returns 500.

Per this PR's own contract, v6-core treats non-2xx as retryable — so a permanently malformed request is retried indefinitely instead of being rejected once.

Suggested fix: guard if (!emailData) throw new StandardError({ …, httpStatusCode: 400 }), or mark the segment schema .required().

4. Ortto's rejection body is no longer logged, making 400s undiagnosable
src/adapters/emailAdapter/orttoAdapter.ts:53

The sanitized log keeps status and e.message, but for an Axios 4xx e.message is only "Request failed with status code 400". Ortto puts the actual cause — which field or attribute it rejected — in e.response.data, which contains neither the X-Api-Key header nor any contact PII the comment is guarding against.

Combined with P0 above, an operator sees an endless 502 loop and cannot tell whether the custom field, the activity, or an attribute is the problem.

Suggested fix: add responseBody: axios.isAxiosError(e) ? e.response?.data : undefined.

5. The "confirmed upsert" guarantee has silent-success holes
src/services/notificationService.ts:365

The 502 fires only inside if (data) and inside the && segmentValidator branch at line 354. Two reachable paths return { success: true } without ever calling Ortto:

  • segmentValidator is undefinedSEGMENT_METADATA_SCHEMA_VALIDATOR is declared with a [key: string] index signature and keyed by hand-typed literals, so a key typo, a NULL notification_type.schemaValidator, or an AdminJS edit silently yields undefined and skips the whole block.
  • activityCreator returns undefined (event missing from ORTTO_EVENT_NAMES).

In both cases control falls through to if (isOrttoSpecific) return { success: true, message: ORTTO_SPECIFIC }, v6-core records a confirmed sync, and the contact is never created — the exact false success this change was written to eliminate.

Suggested fix: for this event, treat a missing validator or falsy data as a hard error rather than a success.

6. The 502 path has no test, and the mock adapter makes it untestable
src/services/notificationService.test.ts · src/adapters/emailAdapter/orttoMockAdapter.ts:7

The three new tests cover only activityCreator's payload shape. OrttoMockAdapter.callOrttoActivity returns true unconditionally, so "How to Test" step 4 ("Confirm a failing Ortto call surfaces as a 502") cannot be performed with EMAIL_ADAPTER=mock as step 1 instructs.

Nothing in CI exercises the new throw, so every regression it introduces — the batch abort below, 4xx-as-retryable-502, the silent-success holes — ships green.

Suggested fix: an injectable/stub adapter that can return false, asserting both the 502 for Sync Ortto contact and the preserved fire-and-forget behavior for another Ortto event.


🟡 P2

7. One Ortto 502 rejects the entire bulk batch
src/controllers/v1/notificationsController.ts:92

sendBulkNotification accepts up to 100 notifications and does await Promise.all(...). Before this PR, throws out of sendNotification were deterministic input errors — retrying a batch neither fixed nor duplicated anything. This PR introduces a throw caused by a transient external failure for the first time. If a batch contains one Sync Ortto contact item and Ortto blips, the 502 rejects Promise.all after other items have already run createNotification and queued emails; the endpoint returns 502 and the caller retries the whole batch.

Suggested fix: Promise.allSettled with per-item status in the response.

8. The new 10s timeout silently changes behavior for every pre-existing Ortto event
src/adapters/emailAdapter/orttoAdapter.ts:32

timeout is set on the shared config used by all ~20 events in ORTTO_EVENT_NAMES, not just the sync. Previously axios had no timeout. Under an Ortto latency spike, a call that used to complete in 12s now aborts at 10s — and those callers ignore the returned false, so the activity is lost with only a log line: no 502, no retry, no notification row marking it unsent.

The PR states "All other Ortto events keep their existing fire-and-forget behavior", but their success rate is now bounded by a new 10s ceiling. Suggest scoping the timeout to this event, or confirming p99 Ortto latency before picking 10s.

9. email isn't validated as an email, though it is the Ortto identity field
src/utils/validators/segmentAndMetadataValidators.ts:165

Joi.string().required() accepts any non-empty string — verified that { email: 'not-an-email', userId: 42 } passes. The value goes straight into fields['str::email'], Ortto's built-in email identity field, so a malformed address becomes an Ortto rejection and (per P0) a permanent 502 loop for that user. There's also no trim/lowercase, so ' A@B.com ' reaches the canonical-email field verbatim.

Suggested fix: Joi.string().email().required(), optionally .trim().lowercase().

10. The PII-sanitization rationale is contradicted two lines above, and in the caller
src/adapters/emailAdapter/orttoAdapter.ts:39 · src/services/notificationService.ts:347

The catch-block comment says the contact's email/names/v6-user-id must NEVER be logged, but the success path logs each full activity (data.activities.map(a => logger.debug('orttoActivityCall', a))), and sendNotification logs payload: body.segment?.payload with the same fields. The default bunyan level in this repo is debug, so the PII is written anyway on any environment running at that level.

So the sanitization removes diagnostics (see P1 #4) without actually keeping PII out of the logs. Either redact those debug logs too, or stop justifying the loss of e.response?.data on PII grounds.

11. The seed migration isn't idempotent, and it gates server startup
migrations/1732000000000-seedNotificationTypeSyncOrttoContact.ts:33

queryRunner.manager.save with no id performs an INSERT, and NotificationType.name is @Column('text', { nullable: false, unique: true }). If the row already exists — hand-seeded on staging to unblock testing, created via AdminJS, or left by a down()-then-re-apply cycle — up() raises a duplicate-key error.

start:server:staging is npm run db:migrate:run:staging && cd dist && pm2-runtime src/index.js, so a failed migration stops the service from starting at all — on the very deploy this PR designates as a prerequisite for v6-core.

Suggested fix: an idempotent upsert (ON CONFLICT ("name") DO NOTHING, or look up by name first).


🤖 Review generated with Claude Code

… false success

- P0/P1: callOrttoActivity now returns {ok,retryable,status,responseBody};
  sendNotification maps a transient Ortto failure (5xx/timeout/network) to 502
  and a permanent one (4xx — bad payload / unprovisioned field/activity) to 422
  for Sync Ortto contact, so a permanent error isn't retried as if transient.
- P1: log Ortto's rejection body (no api key / no PII) so a 4xx is diagnosable.
- P1: syncOrttoContact schema now userId Joi.number().integer().positive() and
  email Joi.string().trim().lowercase().email(); validateWithJoiSchema returns
  the coerced value and the sync forwards it, so one v6 user always maps to one
  stable merge key (no ' 42 '/'042'/'4e1' duplicates) and a bad email is rejected.
- P1: close the silent-success holes — a missing segment (400), a missing
  segment validator (500), or a falsy activityCreator result (500) now fail the
  Sync Ortto contact request instead of returning {success:true}.
- P2: scope the request timeout to the sync event only (other Ortto events keep
  their prior no-timeout behavior).
- P2: seed migration is now idempotent (find-or-insert) so a pre-existing row
  can't fail up() and block staging startup.
- Tests: adapter 4xx/5xx/network classification + response-body; sync validator
  coercion/rejection; nameless-activity attribute omission; safe env cleanup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ae2079

ae2079 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @RamRamez — thorough review. Addressed in 9973664. Point by point:

P0 #1 (4xx→502 retried forever): callOrttoActivity now returns {ok, retryable, status, responseBody}; sendNotification maps a transient Ortto failure (5xx / timeout / network) to 502 and a permanent one (4xx) to 422 for Sync Ortto contact. Note on the deploy window specifically: while Ortto is unconfigured it 400s → 422, and v6-core does keep retrying (every 30 min via its reconcile cron) — that's intentional self-heal (it converges the moment the field/activity is created). The 4xx/5xx split is for correct semantics + diagnostics; a genuinely permanent 4xx is now far less likely because bad email/userId are rejected before Ortto (P1 #2/#9). Also documented the required activity attributes in the Ortto-prereqs.

P1 #2 (userId coercion → duplicate merge keys): schema is now Joi.number().integer().positive(), validateWithJoiSchema returns Joi's coerced value, and the sync forwards it — so ' 42 '/'042'/42 all collapse to one str:cm:v6-user-id = '42', and -1.5/4e1/non-numeric are rejected.

P1 #3 (missing segment → 500 loop): explicit if (!emailData) guard → 400 for the sync (not a retryable 500, not a silent success).

P1 #4 (rejection body not logged): now logs responseBody (e.response?.data) — it carries neither the api key nor contact PII, and is the only way to see which field/attribute Ortto rejected. Still never logs data or the raw Axios error/config.

P1 #5 (silent-success holes): for the sync event, a missing segment validator (500) and a falsy activityCreator result (500) now throw instead of falling through to {success:true}.

P1 #6 (no test for the throw): added orttoAdapter.test.ts (2xx / 4xx-non-retryable+body / 5xx-retryable / connection-failure-retryable) and validator tests; OrttoMockAdapter now exposes nextResult so the 502 path is drivable. The end-to-end sendNotification 502 assertion still needs the DB-backed harness (runs in CI).

P2 #8 (global timeout): the timeout is now applied only to the sync event; all other Ortto events keep their prior no-timeout behavior.

P2 #9 (email not validated): Joi.string().trim().lowercase().email().required().

P2 #10 (PII-log contradiction): resolved by #4 — the catch comment no longer justifies withholding response.data on PII grounds (we now log it, sans PII/headers). The pre-existing debug-level logging of the full activity/payload predates this PR and applies to all Ortto events, so I left it out of scope.

P2 #11 (non-idempotent migration gates startup): up() is now find-or-insert on the unique name, so a pre-seeded row no longer fails the migration / blocks start:server:staging.

Deferred — P2 #7 (bulk Promise.all): the contact sync is sent as a single notification (v6-core posts one at a time), so it never rides sendBulkNotification. Switching that path to Promise.allSettled changes the response contract for every bulk caller, so I kept it out of this PR — happy to do it as a follow-up if you'd like.

tsc clean; new logic validated standalone (validator + adapter classification).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
migrations/1732000000000-seedNotificationTypeSyncOrttoContact.ts (1)

47-50: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Preserve notification types that this migration did not insert.

Lines 35-40 accept a pre-existing row and skip insertion. TypeORM still marks the migration as applied. Line 49 then deletes that pre-existing AdminJS or manually seeded row during rollback.

Record seed ownership and delete only a row created by this migration. If ownership cannot be persisted, make down preserve the row rather than deleting data this migration does not own.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@migrations/1732000000000-seedNotificationTypeSyncOrttoContact.ts` around
lines 47 - 50, Update the migration’s up/down logic around the notification type
seed to track whether this migration actually inserted the “Sync Ortto contact”
row. Make down delete only a row owned by this migration, and preserve
pre-existing rows when insertion was skipped or ownership was not persisted.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/adapters/emailAdapter/orttoMockAdapter.ts`:
- Around line 12-17: Update callOrttoActivity in OrttoMockAdapter so
logger.debug no longer receives the complete data payload; log only
non-sensitive activity identifiers and the microService, ensuring mock-adapter
logs do not retain contact PII.

In `@src/services/notificationService.ts`:
- Around line 366-370: Move the isSyncOrttoContact determination before the
duplicate-track check in the notification handling flow. Update the
duplicate-track success path to bypass its short-circuit for SYNC_ORTTO_CONTACT,
allowing validation and the Ortto upsert to run; preserve existing duplicate
behavior for other events. Add a regression test covering contact sync with an
existing trackId.

---

Outside diff comments:
In `@migrations/1732000000000-seedNotificationTypeSyncOrttoContact.ts`:
- Around line 47-50: Update the migration’s up/down logic around the
notification type seed to track whether this migration actually inserted the
“Sync Ortto contact” row. Make down delete only a row owned by this migration,
and preserve pre-existing rows when insertion was skipped or ownership was not
persisted.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: dc956b8a-200f-409e-9e6e-0dfc366196c2

📥 Commits

Reviewing files that changed from the base of the PR and between d3dc744 and 9973664.

📒 Files selected for processing (10)
  • migrations/1732000000000-seedNotificationTypeSyncOrttoContact.ts
  • src/adapters/emailAdapter/orttoAdapter.test.ts
  • src/adapters/emailAdapter/orttoAdapter.ts
  • src/adapters/emailAdapter/orttoAdapterInterface.ts
  • src/adapters/emailAdapter/orttoMockAdapter.ts
  • src/services/notificationService.test.ts
  • src/services/notificationService.ts
  • src/utils/errorMessages.ts
  • src/utils/validators/segmentAndMetadataValidators.ts
  • src/validators/schemaValidators.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/utils/validators/segmentAndMetadataValidators.ts
  • src/utils/errorMessages.ts

Comment thread src/adapters/emailAdapter/orttoMockAdapter.ts
Comment thread src/services/notificationService.ts Outdated
…pter log

- notificationService: determine isSyncOrttoContact before the duplicate-trackId
  check and exclude the sync event from that short-circuit, so a duplicate
  trackId can never skip the Ortto upsert and return a false success (v6-core
  would mark the contact synced and stop retrying). The sync sends no trackId
  today; this makes the guarantee explicit.
- orttoMockAdapter: log only activity ids + microservice, never the full
  activity payload (email / names / v6-user-id) — no contact PII in logs even
  under EMAIL_ADAPTER=mock (CWE-532).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ae2079

ae2079 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Addressed both in 2357755:

  • Duplicate-trackId short-circuit: isSyncOrttoContact is now determined before the findNotificationByTrackId check, and the sync event is excluded from that early {success:true} return — a duplicate trackId can no longer skip the Ortto upsert. (The sync sends no trackId today, so this is defense-in-depth to keep the confirmed-upsert guarantee unbreakable.)
  • Mock-adapter PII log: OrttoMockAdapter now logs only the activity ids + microservice, never the full activity payload (email / names / v6-user-id) — no contact PII in logs even under EMAIL_ADAPTER=mock (CWE-532).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants