fix(webhooks): emit domain events from auth, module, and reward flows - #18
Merged
merlik787-droi merged 1 commit intoAug 19, 2026
Conversation
Wire WebhookService.queueEvent into the three request paths that were documented but never produced events: user.registered on registration, module.completed on quiz completion, and reward.issued on completed withdrawals. Add employer.contact_attempt to the WebhookEventType union, document the emitted events in docs/API.md, and assert each producer invokes queueEvent in controller tests.
9 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #8
WebhookService.queueEventexisted but was never invoked by any request path, so endpoints registered formodule.completed,reward.issued, oruser.registeredreceived nothing. This PR wires the three producer sites (registration, module completion, reward issuance) to enqueue events through the service, reconcilesemployer.contact_attemptinto theWebhookEventTypecontract, corrects the documented event list, and adds a producer test for each site. Delivery stays on the existing on-demandprocessQueuemodel: every producer enqueues fire-and-forget, so the HTTP response is never blocked on subscriber latency.Why
Before this change the only
webhookDeliverywrites outside tests/seed came fromemployer.controller.ts'scontactCandidate, which wrote directly via Prisma and bypassed the service entirely.queueEventhad no caller insrc/, so the documented outbound-webhook feature never fired, andemployer.contact_attemptwas emitted off-contract (absent from theWebhookEventTypeunion). The producers now reuse the existingqueueEvent+ asyncprocessQueuepath instead of introducing a second delivery mechanism. The employer outreach write is kept direct and documented in-code: its system endpoint URL is internal-only and must never trigger an outbound HMAC-signed HTTP delivery.What was built
src/controllers/auth.controller.tsuser.registeredafter a successful registration (fire-and-forget, before the 201 response).src/controllers/module.controller.tsmodule.completedafter a completion is recorded (fire-and-forget).src/controllers/reward.controller.tsreward.issuedwhen a withdrawal completes (status === 'completed'), after the response is sent.src/types/webhook.types.tsemployer.contact_attempttoWebhookEventTypeso the union lists every emitted event.src/controllers/employer.controller.tsWebhookDeliveryinstead ofqueueEvent.docs/API.mduser.completed_module,credential.verified) with the actually-emitted events.Tests (each producer asserted, with a mirror under
integrations/perdocs/DEVELOPING.md):tests/auth.controller.test.tsuser.registeredis enqueued withuserId/email/username/role.tests/unit/module.controller.test.tsmodule.completedis enqueued withuserId/moduleId/moduleTitle/score/reward.tests/unit/reward.controller.test.tsreward.issuedis enqueued on a completed withdrawal, and not enqueued on withdrawal failure.Integration changes outside the producers
src/controllers/employer.controller.ts— comment only; no behavior change.src/types/webhook.types.ts—WebhookEventTypegained one member.docs/API.md— webhook event list corrected.Acceptance criteria coverage
Producers
module.completeddelivery for every active endpoint subscribed to it. (src/controllers/module.controller.tscompleteModule→queueEvent;tests/unit/module.controller.test.ts)user.registereddelivery. (src/controllers/auth.controller.tsregister;tests/auth.controller.test.ts)reward.issueddelivery. (src/controllers/reward.controller.tswithdraw— the only controller-reachable on-chain reward issuance path;tests/unit/reward.controller.test.ts)Contract
WebhookEventTypeincludes every event type the application emits, includingemployer.contact_attempt. (src/types/webhook.types.ts)Delivery
processQueueruns asynchronously. (each producer callsqueueEvent(...).catch(...)withoutawait;queueEventfiresprocessQueue()fire-and-forget)checkEndpointHealthstill deactivates an endpoint after 10 consecutive failures. (src/services/webhook.service.tsis unmodified in this PR)Tests
queueEventwith the expected event type and payload, without requiring a live subscriber. (tests/auth.controller.test.ts,tests/unit/module.controller.test.ts,tests/unit/reward.controller.test.ts—WebhookServiceis mocked, no network)tests/services/webhook.service.spec.tsstill passes. (279/279 inpnpm test:ci)Documentation
docs/API.md's webhook section lists the actually-emitted events. (user.registered,module.completed,reward.issued,employer.contact_attempt)Test plan
pnpm test:ci— 279/279 passing (1 new test file for themodule.completedproducer)pnpm exec tsc --noEmit— no type errorspnpm lint— no errors or warningspnpm build— succeedsEnv vars / Notes
No new environment variables or config keys.
employer.contact_attemptremains a directWebhookDeliverywrite (an audit log) rather than an outbound delivery; this is intentional and documented insrc/controllers/employer.controller.ts.reward.issuedis emitted from the withdrawal-completion path becauseRewardService.claimRewardis not wired to any controller — withdrawal is the only request path that actually issues rewards on-chain today.