Skip to content

feat: issues 175 171 137 136#289

Merged
ScriptedBro merged 4 commits into
DelegoLabs:mainfrom
od-hunter:feat/issues-175-171-137-136
Jul 13, 2026
Merged

feat: issues 175 171 137 136#289
ScriptedBro merged 4 commits into
DelegoLabs:mainfrom
od-hunter:feat/issues-175-171-137-136

Conversation

@od-hunter

@od-hunter od-hunter commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Closes #175
Closes #171
Closes #137
Closes #136

Summary by CodeRabbit

  • New Features

    • Push subscriptions now track delivery health and automatically remove repeatedly failing or stale subscriptions, while preserving legacy subscription data.
    • Email templates now validate required placeholders and prevent sending partially rendered messages.
    • Escrow deposits with complete order metadata emit a metadata event for indexing.
    • Merchants can retrieve read-only escrow receipts, including status and release eligibility.
  • Bug Fixes

    • Missing email template variables are reported clearly and treated as non-retryable failures.
  • Tests

    • Added coverage for notification cleanup, template errors, escrow receipts, and metadata events.

Emit the stored order metadata hash at escrow creation so indexers can
link contract state to off-chain order records (closes DelegoLabs#175).
Expose get_merchant_receipt with release eligibility for merchant
dashboards and settlement checks (closes DelegoLabs#171).
Track delivery failures and remove expired or repeatedly failing push
subscriptions via cleanupPushSubscriptions (closes DelegoLabs#137).
Return TemplateRenderResult and refuse to send emails with missing
template variables or failed renders (closes DelegoLabs#136).
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds structured email template rendering errors, tracked push-subscription cleanup and failure recording, escrow metadata events, and a merchant receipt getter. Tests, documentation, and ledger snapshots cover notification behavior and escrow lifecycle states.

Changes

Notification delivery resilience

Layer / File(s) Summary
Template rendering and email dispatch
apps/backend/notifications/templates/*, apps/backend/notifications/email/*, apps/backend/notifications/README.md
Template rendering validates required placeholders, returns TemplateRenderResult, prevents partial email dispatch, and classifies rendering failures as permanent.
Tracked push subscription health
apps/backend/notifications/push/*, apps/backend/notifications/README.md
Push records track failure and staleness metadata, support legacy JSON, apply configurable cleanup thresholds, and return cleanup statistics.
Tracked push dispatcher integration
apps/backend/notifications/src/*
Redis persistence, cleanup, subscription lookup, and delivery failures now operate on tracked push records.

Escrow metadata and merchant receipts

Layer / File(s) Summary
Escrow metadata event emission
contracts/escrow/src/lib.rs, contracts/escrow/src/test.rs, contracts/escrow/test_snapshots/test/test/*
Deposits with complete metadata emit EscrowMetadataEvent; tests and snapshots verify complete, partial, and absent metadata cases.
Merchant receipt getter and lifecycle coverage
contracts/escrow/src/lib.rs, contracts/escrow/src/integration_tests.rs, contracts/escrow/test_snapshots/integration_tests/*
get_merchant_receipt returns merchant, buyer, status, and release eligibility, with coverage for funded, disputed, released, refunded, and missing escrows.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

Notification delivery

sequenceDiagram
  participant ApprovalDispatcher
  participant Redis
  participant PushSubscription
  participant Cleanup
  ApprovalDispatcher->>Redis: read tracked subscriptions
  ApprovalDispatcher->>PushSubscription: send notification
  PushSubscription-->>ApprovalDispatcher: delivery failure
  ApprovalDispatcher->>Redis: store incremented failure metadata
  Cleanup->>Redis: remove stale or repeatedly failing records
Loading

Escrow metadata and receipts

sequenceDiagram
  participant DepositCaller
  participant EscrowContract
  participant Indexer
  participant Merchant
  DepositCaller->>EscrowContract: deposit with order metadata
  EscrowContract-->>Indexer: EscrowMetadataEvent
  Merchant->>EscrowContract: get_merchant_receipt(escrow_id)
  EscrowContract-->>Merchant: MerchantEscrowReceipt
Loading

Possibly related PRs

  • DelegoLabs/Delego#275 — modifies escrow metadata persistence in the same contract flow extended by this PR.
  • DelegoLabs/Delego#279 — changes the email retry and classification flow extended here for template rendering failures.
  • DelegoLabs/Delego#229 — modifies the same notification dispatcher flow as push failure tracking.

Suggested labels: size/XL

Poem

A bunny hops through templates bright,
Missing vars no longer take flight.
Push trails fade when failures grow,
Escrow bells ring metadata’s glow.
Receipts bloom for merchants clear—
Hop, hop, reliable mail is here!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title references linked issues but doesn't describe the actual changes, so it's too vague for a changelog scan. Rename it to a concise summary of the main change, such as adding escrow metadata events, merchant receipts, push cleanup, and template rendering validation.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements the four linked features and adds focused success/failure tests and docs for each.
Out of Scope Changes check ✅ Passed The README updates and snapshot additions support the linked features, and no unrelated changes stand out.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@ScriptedBro ScriptedBro changed the title Feat/issues 175 171 137 136 feat: issues 175 171 137 136 Jul 13, 2026

@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: 5

Caution

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

⚠️ Outside diff range comments (2)
apps/backend/notifications/src/permissionEventListener.ts (1)

751-760: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Skip malformed push subscriptions instead of dropping the whole set

parseTrackedPushSubscription throws on bad JSON, and the outer try/catch turns that into [], so one corrupt Redis member drops every other valid subscription for this user. Parse each member defensively and keep the rest.

🤖 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 `@apps/backend/notifications/src/permissionEventListener.ts` around lines 751 -
760, Update getUserPushSubscriptions so each Redis member is parsed defensively,
skipping malformed entries while retaining valid subscriptions. Move error
isolation into the members.map processing around parseTrackedPushSubscription,
and ensure one parsing failure does not cause the outer try/catch to return an
empty set for all members.
apps/backend/notifications/src/dispatcher.ts (1)

53-65: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Handle malformed Redis members in removePushSubscription
parseTrackedPushSubscription already accepts legacy bare PushSubscription JSON, but a single invalid member can still throw and stop the loop. Catch per-item parse errors so one corrupt record doesn’t block savePushSubscription from adding the new entry.

🤖 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 `@apps/backend/notifications/src/dispatcher.ts` around lines 53 - 65, The
removePushSubscription loop currently aborts when parseTrackedPushSubscription
encounters a malformed Redis member. Wrap each member’s parsing and endpoint
comparison in per-item error handling so invalid records are skipped while valid
matching subscriptions are still removed and the loop continues, allowing
savePushSubscription to add new entries.
🧹 Nitpick comments (4)
contracts/escrow/src/lib.rs (1)

1142-1169: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Field escrow_id stores the 32‑byte order_id, not the numeric escrow_id argument.

get_merchant_receipt is queried by the u64 escrow_id, but the returned MerchantEscrowReceipt.escrow_id is set from record.order_id (a BytesN<32>). This mirrors ReleaseEligibility, so it may be intentional, but the same name meaning two different identifiers is easy to misread on the consumer side. Consider renaming the struct field to order_id for clarity, or documenting the mapping at the call boundary.

This applies equally to MerchantEscrowReceipt (Line 319) and EscrowMetadataEvent (Line 79).

🤖 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 `@contracts/escrow/src/lib.rs` around lines 1142 - 1169, Clarify the identifier
naming across MerchantEscrowReceipt and EscrowMetadataEvent: rename the
receipt/event field currently named escrow_id to order_id where it stores
record.order_id, and update get_merchant_receipt and all consumers or
constructors accordingly. Preserve the numeric escrow_id lookup and any separate
fields that represent the u64 key.
contracts/escrow/src/integration_tests.rs (1)

755-769: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing coverage: Funded + timed-out → release_eligible = false.

Per the upstream release_block_reason logic, a Funded escrow becomes release-ineligible once env.ledger().sequence() >= record.timeout_ledger, independent of status transitions. None of the new tests advance the ledger sequence, so only the "not yet timed out" branch of Funded is exercised — the timeout branch of the release-eligibility logic (a core part of issue #171) remains untested.

♻️ Suggested additional test
/// Success path: funded escrow past timeout is not release-eligible.
#[test]
fn test_get_merchant_receipt_funded_timeout_not_eligible() {
    let t = TestEnv::setup();
    let client = EscrowContractClient::new(&t.env, &t.escrow_contract_id);

    let eid = deposit_escrow(&t, 1000, 100);
    t.env.ledger().with_mut(|li| li.sequence_number = 100);

    let receipt = client.get_merchant_receipt(&eid);
    assert_eq!(receipt.status, EscrowStatus::Funded);
    assert!(!receipt.release_eligible);
}
🤖 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 `@contracts/escrow/src/integration_tests.rs` around lines 755 - 769, Add a
timeout-branch integration test alongside
test_get_merchant_receipt_funded_state: after deposit_escrow creates a Funded
escrow, advance the test ledger sequence to the escrow timeout boundary, call
get_merchant_receipt, and assert the status remains EscrowStatus::Funded while
release_eligible is false.
apps/backend/notifications/templates/index.test.ts (1)

100-155: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Good coverage overall; one failure branch is untested.

The "unsubstituted placeholders remain after replace" branch in renderTemplateContent (triggered when data doesn't cover every {{key}}, or via a replaceAll special-pattern reinsertion) has no direct test, nor does the renderNamedTemplate read/render exception branch.

🤖 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 `@apps/backend/notifications/templates/index.test.ts` around lines 100 - 155,
Add tests covering the unsubstituted-placeholder failure branch in
renderTemplateContent, including a replacement pattern that leaves {{key}}
tokens intact if applicable, and assert the returned error and absent html. Also
add a renderNamedTemplate test that exercises a template file read/render
exception and verifies the expected error result, using the existing helper APIs
and result contract.
apps/backend/notifications/push/index.ts (1)

173-192: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Minor inconsistency: catch branch drops entries from both retained and removed.

Malformed-shape entries (lines 179-183) are counted as failed and pushed into removed, but entries caught via the catch block (191) are only counted in failed — they vanish from both output arrays despite being counted in scanned. For consistency (and to avoid silently dropped subscriptions if a caller rewrites Redis state from retained/removed), consider pushing to removed here too. Low priority since the preceding validation likely makes this branch unreachable in practice.

🤖 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 `@apps/backend/notifications/push/index.ts` around lines 173 - 192, Update the
catch branch in the subscriptions loop to push the caught `tracked` entry into
`removed` alongside incrementing `failed`, matching the malformed-entry handling
and preventing it from being omitted from both output arrays.
🤖 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 `@apps/backend/notifications/push/index.ts`:
- Around line 42-50: Validate the environment-derived values in
getMaxPushFailures and getPushStaleMs so non-numeric or otherwise invalid values
cannot produce NaN and disable cleanup. Fall back to the existing defaults (or
fail explicitly) while preserving valid overrides, and ensure
shouldRemovePushSubscription continues evaluating both removal rules with usable
numeric thresholds.

In `@apps/backend/notifications/src/dispatcher.ts`:
- Around line 190-199: Update the sendPushNotification failure handler so
recordPushDeliveryFailure(tracked) is added to Redis with redis.sadd before
removing raw with redis.srem. Preserve the existing failure-record update and
cleanup behavior, ensuring an error during the add cannot cause the original
subscription to be deleted.
- Around line 71-99: Update cleanupUserPushSubscriptions to safely handle
malformed Redis members without aborting the user cleanup: isolate
parseTrackedPushSubscription failures and treat invalid records as discarded.
Replace the separate srem-all and sadd operations with one atomic MULTI/Lua
update that removes only discarded serialized members while preserving retained
subscriptions, including when the update fails partway.
- Around line 150-157: Update the Redis subscription processing in
dispatchTransactionApproval to isolate parseTrackedPushSubscription failures per
member. Skip malformed entries before building or iterating trackedList, while
continuing to process valid subscriptions so one corrupt Redis record cannot
abort dispatch.

In `@apps/backend/notifications/templates/index.ts`:
- Around line 95-121: Update renderTemplateContent so each data value is
HTML-escaped before replacing its corresponding placeholder, preventing
user-controlled markup injection. Use the replacement callback form of
replaceAll rather than a string replacement, ensuring dollar-sign sequences in
values are inserted literally; preserve the existing missing-variable and
unsubstituted-placeholder checks.

---

Outside diff comments:
In `@apps/backend/notifications/src/dispatcher.ts`:
- Around line 53-65: The removePushSubscription loop currently aborts when
parseTrackedPushSubscription encounters a malformed Redis member. Wrap each
member’s parsing and endpoint comparison in per-item error handling so invalid
records are skipped while valid matching subscriptions are still removed and the
loop continues, allowing savePushSubscription to add new entries.

In `@apps/backend/notifications/src/permissionEventListener.ts`:
- Around line 751-760: Update getUserPushSubscriptions so each Redis member is
parsed defensively, skipping malformed entries while retaining valid
subscriptions. Move error isolation into the members.map processing around
parseTrackedPushSubscription, and ensure one parsing failure does not cause the
outer try/catch to return an empty set for all members.

---

Nitpick comments:
In `@apps/backend/notifications/push/index.ts`:
- Around line 173-192: Update the catch branch in the subscriptions loop to push
the caught `tracked` entry into `removed` alongside incrementing `failed`,
matching the malformed-entry handling and preventing it from being omitted from
both output arrays.

In `@apps/backend/notifications/templates/index.test.ts`:
- Around line 100-155: Add tests covering the unsubstituted-placeholder failure
branch in renderTemplateContent, including a replacement pattern that leaves
{{key}} tokens intact if applicable, and assert the returned error and absent
html. Also add a renderNamedTemplate test that exercises a template file
read/render exception and verifies the expected error result, using the existing
helper APIs and result contract.

In `@contracts/escrow/src/integration_tests.rs`:
- Around line 755-769: Add a timeout-branch integration test alongside
test_get_merchant_receipt_funded_state: after deposit_escrow creates a Funded
escrow, advance the test ledger sequence to the escrow timeout boundary, call
get_merchant_receipt, and assert the status remains EscrowStatus::Funded while
release_eligible is false.

In `@contracts/escrow/src/lib.rs`:
- Around line 1142-1169: Clarify the identifier naming across
MerchantEscrowReceipt and EscrowMetadataEvent: rename the receipt/event field
currently named escrow_id to order_id where it stores record.order_id, and
update get_merchant_receipt and all consumers or constructors accordingly.
Preserve the numeric escrow_id lookup and any separate fields that represent the
u64 key.
🪄 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: d407039b-aa3e-432e-aff6-62f4b2a3e3df

📥 Commits

Reviewing files that changed from the base of the PR and between 5aff918 and fa77eed.

📒 Files selected for processing (21)
  • apps/backend/notifications/README.md
  • apps/backend/notifications/email/errorClassifier.ts
  • apps/backend/notifications/email/index.test.ts
  • apps/backend/notifications/email/index.ts
  • apps/backend/notifications/push/index.test.ts
  • apps/backend/notifications/push/index.ts
  • apps/backend/notifications/src/dispatcher.ts
  • apps/backend/notifications/src/permissionEventListener.ts
  • apps/backend/notifications/templates/index.test.ts
  • apps/backend/notifications/templates/index.ts
  • contracts/escrow/src/integration_tests.rs
  • contracts/escrow/src/lib.rs
  • contracts/escrow/src/test.rs
  • contracts/escrow/test_snapshots/integration_tests/test_get_merchant_receipt_disputed_state.1.json
  • contracts/escrow/test_snapshots/integration_tests/test_get_merchant_receipt_funded_state.1.json
  • contracts/escrow/test_snapshots/integration_tests/test_get_merchant_receipt_not_found.1.json
  • contracts/escrow/test_snapshots/integration_tests/test_get_merchant_receipt_refunded_state.1.json
  • contracts/escrow/test_snapshots/integration_tests/test_get_merchant_receipt_released_state.1.json
  • contracts/escrow/test_snapshots/test/test/test_deposit_with_metadata_emits_metadata_event.1.json
  • contracts/escrow/test_snapshots/test/test/test_deposit_with_partial_metadata_does_not_emit_metadata_event.1.json
  • contracts/escrow/test_snapshots/test/test/test_deposit_without_metadata_does_not_emit_metadata_event.1.json

Comment thread apps/backend/notifications/push/index.ts
Comment thread apps/backend/notifications/src/dispatcher.ts
Comment thread apps/backend/notifications/src/dispatcher.ts
Comment thread apps/backend/notifications/src/dispatcher.ts
Comment thread apps/backend/notifications/templates/index.ts
@ScriptedBro
ScriptedBro merged commit 8f81d6d into DelegoLabs:main Jul 13, 2026
11 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants