Skip to content

Wire distribution event handlers to persistence and add API tests - #80

Merged
pragmaticAweds merged 4 commits into
Fundable-Protocol:devfrom
vickydve:dev
Jun 30, 2026
Merged

pragmaticAweds merged 4 commits into
Fundable-Protocol:devfrom
vickydve:dev

Conversation

@vickydve

@vickydve vickydve commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Two scoped changes for the distributions domain:

Indexer (#53) — wire distribution event handlers to persistence

  • Add DistributionBatch and ClaimAction TypeORM entities plus an initial migration (indexer/distributions/src/db/).
  • Add DistributionRepository with explicit createBatch / recordClaim / setStatus APIs.
  • Convert the distribution handlers to repository-backed factories that:
    • persist created batches after validating required payload fields,
    • record claims without double-counting (unique claim event identity + increment only on insert),
    • apply paused/resumed status changes,
    • coordinate indexed event identity through the shared EventRepository so replayed events are idempotent.
  • Remove the persistence TODOs.

Backend API (#55) — distribution endpoint tests and consistent error responses

  • Align controller responses with the shared sendSuccess / sendError helpers.
  • Return 503 DB_NOT_READY and 404 DISTRIBUTION_NOT_FOUND instead of raw 500s; add a coded not-found error in the service.
  • Replace console.* with the project logger.
  • Make the controller service resolver injectable so endpoints are testable without a live DB.

Area

  • Backend API (src/)
  • Indexer common infrastructure (indexer/common/)
  • Streams indexer (indexer/streams/)
  • Distributions indexer (indexer/distributions/)
  • Tooling, docs, CI, or Docker

Scope

  • This PR addresses one scoped issue or task
  • Unrelated formatting, generated files, and follow-up work were left out
  • Backend and indexer package boundaries were respected

Verification

  • bun run type-check
  • bun run test — all 58 tests pass; see note on the pre-existing coverage gate
  • bun run lint
  • bun run indexer:type-check
  • bun run indexer:test
  • bun run indexer:lint

Indexer Safety

  • Event processing changes are idempotent or do not affect event processing
  • Cursor changes advance only after successful processing
  • Event names and payload shapes were confirmed from contracts, if relevant
  • Backfill and replay behavior was considered, if relevant

Idempotency is enforced via the shared indexed_event identity gate plus a unique (txHash, ledgerNumber, eventIndex) constraint on claims. Handler tests cover created, claimed, paused, resumed, invalid payload, and duplicate replay paths.

Notes

Closes #53
Closes #55

Test coverage caveat: all 58 backend tests pass, but bun run test enforces a global 90% coverage threshold that is already red on dev (~74%) due to low coverage in unrelated campaign files (campaign.entity.ts, campaign.service.ts). The new distribution service/controller/validation tests are thorough (distribution code sits at ~94% when measured), and consistent with the donation domain precedent they were not added to the shared c8.include. Raising the global threshold to green would require campaign-domain work outside the scope of these two issues.

Summary by CodeRabbit

  • New Features

    • Added persistent tracking for distribution batches, claims, and status changes.
    • Distribution endpoints now support creating, updating, listing, pausing, and resuming with improved data handling.
  • Bug Fixes

    • Improved duplicate-event handling to prevent double-processing of distribution actions.
    • Added clearer error responses when the service is unavailable or a distribution isn’t found.
  • Tests

    • Expanded coverage for distribution APIs, service behavior, validation rules, and event processing.

claude and others added 2 commits June 30, 2026 05:49
…ests

Indexer (#53): Wire distribution event handlers to durable persistence
- Add DistributionBatch and ClaimAction entities plus an initial migration
- Add DistributionRepository with explicit createBatch/recordClaim/setStatus APIs
- Convert distribution handlers to repository-backed factories that:
  - persist created batches after validating required payload fields
  - record claims without double-counting via a unique claim event identity
  - apply paused/resumed status changes
  - coordinate indexed event identity through the shared EventRepository so
    replayed events are idempotent
- Replace the persistence TODOs with real writes
- Add repository-backed handler tests and a repository unit test covering
  created, claimed, paused, resumed, invalid payload, and duplicate replay

API (#55): Distribution endpoint tests and consistent error responses
- Align controller responses with shared sendSuccess/sendError helpers
- Return 503 DB_NOT_READY and 404 DISTRIBUTION_NOT_FOUND instead of raw 500s
- Replace console.* with the project logger; add a coded not-found error
- Make the controller service resolver injectable for testing
- Add controller, service, and validation tests covering success, validation,
  not-found, db-not-ready, and internal-error paths

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bjxdi78eokeEiy3MG9EuVy
…ence-hq3gzb

feat(distributions): wire event handlers to persistence and add API tests
@drips-wave

drips-wave Bot commented Jun 30, 2026

Copy link
Copy Markdown

@vickydve Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

An error occurred during the review process. Please try again later.

📝 Walkthrough

Walkthrough

Adds a full TypeORM persistence layer (entities, migration, repository) for distribution batches and claim actions in the indexer; rewires all four distribution event handlers into dependency-injected factories with idempotency gating; refactors the distribution API controller to use resolver injection and centralized error mapping; adds DistributionNotFoundError; and introduces test suites for handlers, repository, controller, service, and validation schemas.

Changes

Indexer Distribution Persistence and Handler Wiring

Layer / File(s) Summary
TypeORM entities and migration
indexer/distributions/package.json, indexer/distributions/src/db/entity/DistributionBatch.ts, indexer/distributions/src/db/entity/ClaimAction.ts, indexer/distributions/src/db/migrations/00001_InitialDistributionsSchema.ts
Adds typeorm dependency; defines DistributionStatus enum, DistributionBatch and ClaimAction entities; creates the migration that builds both tables with composite unique constraints, indexes, and cascade delete.
DistributionRepository and unit tests
indexer/distributions/src/db/repository.ts, indexer/distributions/src/db/repository.test.ts
Defines CreateBatchInput, RecordClaimInput, SetStatusInput, and DistributionPersistence interface; implements createBatch (orIgnore), recordClaim (transactional, duplicate-guarded increment of claimedAmount), and setStatus; full unit tests with a mock query-builder harness.
Persistence contracts and deriveEventIndex
indexer/distributions/src/handlers/persistence.ts, indexer/distributions/src/handlers/index.ts, indexer/distributions/src/index.ts
Adds EventIdentityStore and DistributionHandlerDeps interfaces; exports deriveEventIndex; updates module re-exports to expose entities, repository, and handler factory names.
Dependency-injected handler factories
indexer/distributions/src/handlers/distribution-created.handler.ts, indexer/distributions/src/handlers/tokens-claimed.handler.ts, indexer/distributions/src/handlers/distribution-pause.handler.ts, indexer/distributions/src/handlers/types.ts
Replaces static exported handlers with create*Handler(deps) factories; each factory validates payload fields, gates on isEventProcessed, persists via distributions.*, and records processed event identity; non-retriable failures on missing/invalid fields.
Handler factory tests
indexer/distributions/src/handlers/distribution-handlers.test.ts
Rewrites test suite with makeDeps() harness backed by an in-memory processed set; covers success, validation failures, idempotency replay, and retriable errors for all four handlers.
Formatting-only (common and streams)
indexer/common/src/handlers/registry.ts, indexer/common/src/handlers/registry.test.ts, indexer/common/src/handlers/types.ts, indexer/streams/src/handlers/stream-*.handler.ts
Condenses multi-line imports and type expressions to single lines; no logic changes.

Distribution API Controller/Service Hardening

Layer / File(s) Summary
DistributionNotFoundError and logger
src/components/v1/distribution/distribution.service.ts
Exports DistributionNotFoundError with stable code = "DISTRIBUTION_NOT_FOUND"; replaces console.error/warn with logger; re-throws DistributionNotFoundError in the update catch block.
Controller resolver injection and centralized error handling
src/components/v1/distribution/distribution.controller.ts, src/components/v1/distribution/distrubtion.routes.ts
Adds DatabaseNotReadyError, DistributionServiceResolver, defaultDistributionServiceResolver, and handleControllerError; converts createDistribution, updateDistribution, and listDistributions to resolver-injected factories using sendSuccess; routes updated to invoke factories.
Controller, service, and validation tests
src/__tests__/distribution.controller.test.ts, src/__tests__/distribution.service.test.ts, src/__tests__/distribution.validation.test.ts
Adds resolver-injection-based controller tests (201/503/500/404/200 and policyMiddleware); service tests for normalization, defaulting, and DistributionNotFoundError; validation schema tests for positive/negative cases.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • #53 – Wire distribution event handlers to persistence: This PR directly implements the full scope: TypeORM entities, migration, DistributionRepository, idempotent handler factories, and handler tests with realistic Soroban payloads.
  • #55 – Add distribution API integration tests and consistent error responses: This PR adds DistributionNotFoundError, DatabaseNotReadyError, resolver-based controller refactor, policyMiddleware test, and controller/service/validation test suites matching all acceptance criteria.

Possibly related PRs

  • Fundable-Protocol/Backend#5: Introduced the original createDistribution controller handler that this PR refactors into a resolver-injected factory.
  • Fundable-Protocol/Backend#19: Implemented updateDistribution and the "Distribution not found" string check that this PR replaces with DistributionNotFoundError.
  • Fundable-Protocol/Backend#49: Modified the same indexer/common/src/handlers/registry.ts and distribution handler entry points that this PR also touches.

Suggested reviewers

  • mubarak23

Poem

🐇 Hoppity-hop through the ledger we go,
Batches persisted in tables below,
Idempotent claims—no double the loot,
Factories injected from leaf to root.
Errors now typed with a code and a name,
Tests green and passing, the rabbit's not lame! 🎉

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The diff also includes unrelated formatting-only edits in indexer/common and indexer/streams that are outside #53/#55. Move the formatting-only common/streams edits into a separate PR or remove them from this changeset.
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: wiring distribution handlers to persistence and adding API tests.
Description check ✅ Passed The description follows the template with Summary, Area, Scope, Verification, Indexer Safety, and Notes filled in.
Linked Issues check ✅ Passed The PR implements the persistence-backed distribution handlers and the API regression/error-handling updates required by #53 and #55.
✨ 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (2)
src/components/v1/distribution/distribution.service.ts (1)

73-77: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Normalize unexpected update failures to the same service contract.

createDistribution and listDistributions translate repository failures into service-level errors, but updateDistribution now rethrows raw repository exceptions. That makes update the odd one out and undercuts the error-handling standardization this PR is adding. Preserve DistributionNotFoundError, then wrap everything else in a single "Failed to update distribution" error.

🤖 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/components/v1/distribution/distribution.service.ts` around lines 73 - 77,
In updateDistribution, keep the existing DistributionNotFoundError passthrough,
but normalize every other unexpected failure to the same service-level contract
used by createDistribution and listDistributions. Update the error handling
around the logger.error path so it logs the original exception details, then
always throws a new Error with the message "Failed to update distribution"
instead of rethrowing raw repository errors.
src/components/v1/distribution/distribution.controller.ts (1)

23-33: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Defer DB wiring so resolver injection actually decouples the controller.

src/__tests__/distribution.controller.test.ts, Lines 4-13 still have to seed DB env vars before this module can even be imported. That means the new resolver injection removes the live-connection requirement, but not the data-source bootstrap coupling. Moving the AppDataSource/entity lookup behind the default resolver path would make injected-controller tests fully standalone.

🤖 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/components/v1/distribution/distribution.controller.ts` around lines 23 -
33, The default resolver in distribution.controller still pulls in AppDataSource
and DistributionEntity at module load, so controller tests cannot import the
module without DB env setup. Move the data-source and repository lookup fully
inside defaultDistributionServiceResolver so only the default path touches
AppDataSource.getRepository(DistributionEntity), while injected resolver paths
remain free of bootstrap coupling.
🤖 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 `@indexer/distributions/src/db/migrations/00001_InitialDistributionsSchema.ts`:
- Around line 7-50: The migration for 00001_InitialDistributionsSchema uses
uuid_generate_v4() in distribution_claim_action without ensuring the uuid-ossp
extension exists, so a clean PostgreSQL setup will fail. Update the migration to
create/enable the UUID extension before the CREATE TABLE for
distribution_claim_action, ideally alongside the existing enum setup in the same
queryRunner.query block, and keep the table creation for distribution_batch and
distribution_claim_action unchanged otherwise.

In `@indexer/distributions/src/db/repository.ts`:
- Around line 29-34: `SetStatusInput.status` in `SetStatusInput` is too broad
for what `setStatus()` can actually persist, because the non-`PAUSED` branch in
the status mapping treats every other value as a resume event. Narrow the
accepted status type to only the states `setStatus()` can handle correctly, and
update the logic in `setStatus()` so `COMPLETED` and `CANCELLED` are either
rejected or mapped to their own persisted fields instead of `resumedAt`; also
make sure the `src/index.ts` re-export reflects the corrected contract.

In `@indexer/distributions/src/handlers/distribution-created.handler.ts`:
- Around line 40-46: The missing-total validation in distribution handling is
ineffective because parseDistributionCreated() still defaults a missing
total_amount to "0", so the handler can proceed with a fabricated value. Update
the parsing/validation flow in parseDistributionCreated() and the
distributionCreated handler so a raw missing total_amount is rejected before
createBatch is called, and ensure the payload.totalAmount check reflects the
actual presence of the field rather than a defaulted value.

In `@indexer/distributions/src/handlers/distribution-pause.handler.ts`:
- Around line 50-61: The pause/resume handlers in distribution-pause.handler.ts
and the matching resume path update status before calling
recordEventProcessed(), which can let a retried older event overwrite a newer
state after partial failure. Refactor the flow around the
setStatus/deps.distributions call and deps.events.recordEventProcessed so the
status change and event-processing mark happen atomically in one repository
operation, or extend SetStatusInput/setStatus to include ledger/event identity
and reject out-of-order updates using that metadata.

---

Nitpick comments:
In `@src/components/v1/distribution/distribution.controller.ts`:
- Around line 23-33: The default resolver in distribution.controller still pulls
in AppDataSource and DistributionEntity at module load, so controller tests
cannot import the module without DB env setup. Move the data-source and
repository lookup fully inside defaultDistributionServiceResolver so only the
default path touches AppDataSource.getRepository(DistributionEntity), while
injected resolver paths remain free of bootstrap coupling.

In `@src/components/v1/distribution/distribution.service.ts`:
- Around line 73-77: In updateDistribution, keep the existing
DistributionNotFoundError passthrough, but normalize every other unexpected
failure to the same service-level contract used by createDistribution and
listDistributions. Update the error handling around the logger.error path so it
logs the original exception details, then always throws a new Error with the
message "Failed to update distribution" instead of rethrowing raw repository
errors.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9615cccd-81d5-475f-a4d3-7119496b0b74

📥 Commits

Reviewing files that changed from the base of the PR and between a91569c and d6cc9f8.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (26)
  • indexer/common/src/handlers/registry.test.ts
  • indexer/common/src/handlers/registry.ts
  • indexer/common/src/handlers/types.ts
  • indexer/distributions/package.json
  • indexer/distributions/src/db/entity/ClaimAction.ts
  • indexer/distributions/src/db/entity/DistributionBatch.ts
  • indexer/distributions/src/db/migrations/00001_InitialDistributionsSchema.ts
  • indexer/distributions/src/db/repository.test.ts
  • indexer/distributions/src/db/repository.ts
  • indexer/distributions/src/handlers/distribution-created.handler.ts
  • indexer/distributions/src/handlers/distribution-handlers.test.ts
  • indexer/distributions/src/handlers/distribution-pause.handler.ts
  • indexer/distributions/src/handlers/index.ts
  • indexer/distributions/src/handlers/persistence.ts
  • indexer/distributions/src/handlers/tokens-claimed.handler.ts
  • indexer/distributions/src/handlers/types.ts
  • indexer/distributions/src/index.ts
  • indexer/streams/src/handlers/stream-cancel.handler.ts
  • indexer/streams/src/handlers/stream-funded.handler.ts
  • indexer/streams/src/handlers/stream-withdrawal.handler.ts
  • src/__tests__/distribution.controller.test.ts
  • src/__tests__/distribution.service.test.ts
  • src/__tests__/distribution.validation.test.ts
  • src/components/v1/distribution/distribution.controller.ts
  • src/components/v1/distribution/distribution.service.ts
  • src/components/v1/distribution/distrubtion.routes.ts

Comment thread indexer/distributions/src/db/repository.ts
Comment thread indexer/distributions/src/handlers/distribution-created.handler.ts
Comment thread indexer/distributions/src/handlers/distribution-pause.handler.ts
@pragmaticAweds

Copy link
Copy Markdown
Contributor

Hi @vickydve

Thank you for your awesome contribution, however after analyzing your implementation, there are some minor fixes to be done. Kindly fix them to merge your PR asap.

Also do not forget to use fundable.finance to offramp.

claude and others added 2 commits June 30, 2026 07:54
- migration: create the uuid-ossp extension before tables that use
  uuid_generate_v4(), so the schema applies on a clean database
- repository: narrow setStatus to ACTIVE/PAUSED so COMPLETED/CANCELLED
  can no longer be passed as type-valid resume writes
- repository: guard status updates with a statusLedger column so a stale
  (out-of-order) pause/resume cannot overwrite newer state after a retry
- handlers: drop the total_amount "0" parser default so a missing amount
  is rejected instead of persisting a fabricated total
- pass the event ledger into setStatus and cover the new paths in tests

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bjxdi78eokeEiy3MG9EuVy
…ence-hq3gzb

fix(distributions): address review feedback on persistence layer
@pragmaticAweds
pragmaticAweds merged commit dedc29b into Fundable-Protocol:dev Jun 30, 2026
1 check 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

Development

Successfully merging this pull request may close these issues.

Add distribution API integration tests and consistent error responses Wire distribution event handlers to persistence

3 participants