- Labels:
refactor, technical-debt, backend
- Area / component:
modules/*/application/sync-*-from-event.ts, modules/*/index.ts
Problem
Every module that filters the shared event bus by originating contract compares event.contractName against a bare string literal ('escrow', 'delivery', 'dispute-resolution', 'fleet', 'identity-reputation') repeated independently in each consumer, and each module's own getTrackedContracts()-style wiring in index.ts independently hardcodes the same string as the value it assigns. No shared constant or literal-union type ties these together, even though the project already uses exactly that pattern for the analogous "canonical name shared between producers and consumers" concept elsewhere.
Current behavior
grep -rln "contractName ===" src/modules/*/application/*.ts confirms the pattern in at least five application-layer files (deliveries, disputes, escrow, fleet, reputation; notifications/fraud-detection add two more via their own switch (event.contractName) dispatch), each independently spelling the exact contract-name string it cares about. src/shared/queue/queues.ts shows the project's own preferred pattern for this exact class of problem — QueueName is a single, shared, as const object whose values producers and consumers both reference, with its own comment explaining why: "Modules reference these constants rather than hardcoding strings, so a rename is a one-line change instead of a grep-and-pray." No equivalent exists for contract names.
Evidence / code location
src/modules/deliveries/application/sync-delivery-from-event.ts:23, src/modules/escrow/application/sync-escrow-from-event.ts:26, src/modules/disputes/application/sync-dispute-from-event.ts:51,54, src/modules/fleet/application/sync-fleet-from-event.ts:25, src/modules/reputation/application/sync-reputation-from-event.ts:45 — each module's own hardcoded event.contractName !== '...'/=== '...' check.
src/modules/{deliveries,escrow,fleet,disputes,reputation}/index.ts's getTrackedContracts()-equivalent wiring — each independently assigns the matching literal as contractName: '...'.
src/shared/queue/queues.ts:10-14 — the QueueName const object, the project's own established pattern for exactly this kind of cross-file string constant, with its own header comment explaining the rationale.
Impact
A single-character typo in any one of these seven-plus locations (e.g., renaming 'dispute-resolution' to 'disputes-resolution' in the indexer's tracked-contract config without updating the disputes module's own comparison) would silently and permanently stop that module from ever matching any event for that contract, with no compile-time error — the strings are compared as plain values, not checked against a shared literal type the compiler could flag a mismatch on.
Expected behavior
Contract names are defined once, as a shared, typed constant every producer and consumer references — mirroring QueueName.
Proposed scope / implementation direction
- Add a
ContractName const object (mirroring QueueName's shape) to src/shared/events/ or src/blockchain/, with one entry per tracked contract (Escrow: 'escrow', Delivery: 'delivery', DisputeResolution: 'dispute-resolution', Fleet: 'fleet', IdentityReputation: 'identity-reputation').
- Type
BlockchainEventEnvelope.contractName as ContractNameValue rather than a bare string, so a typo'd comparison anywhere becomes a compile-time type error rather than a silent runtime no-match.
- Replace every hardcoded literal (both the
index.ts tracked-contract wiring and each application-layer comparison) with the shared constant.
Acceptance criteria
Verification / testing requirements
- Existing per-module sync-from-event specs pass unchanged — this is a pure extraction with no behavior change.
pnpm typecheck confirms the narrowed type doesn't break any existing call site.
refactor,technical-debt,backendmodules/*/application/sync-*-from-event.ts,modules/*/index.tsProblem
Every module that filters the shared event bus by originating contract compares
event.contractNameagainst a bare string literal ('escrow','delivery','dispute-resolution','fleet','identity-reputation') repeated independently in each consumer, and each module's owngetTrackedContracts()-style wiring inindex.tsindependently hardcodes the same string as the value it assigns. No shared constant or literal-union type ties these together, even though the project already uses exactly that pattern for the analogous "canonical name shared between producers and consumers" concept elsewhere.Current behavior
grep -rln "contractName ===" src/modules/*/application/*.tsconfirms the pattern in at least five application-layer files (deliveries,disputes,escrow,fleet,reputation;notifications/fraud-detectionadd two more via their ownswitch (event.contractName)dispatch), each independently spelling the exact contract-name string it cares about.src/shared/queue/queues.tsshows the project's own preferred pattern for this exact class of problem —QueueNameis a single, shared,as constobject whose values producers and consumers both reference, with its own comment explaining why: "Modules reference these constants rather than hardcoding strings, so a rename is a one-line change instead of a grep-and-pray." No equivalent exists for contract names.Evidence / code location
src/modules/deliveries/application/sync-delivery-from-event.ts:23,src/modules/escrow/application/sync-escrow-from-event.ts:26,src/modules/disputes/application/sync-dispute-from-event.ts:51,54,src/modules/fleet/application/sync-fleet-from-event.ts:25,src/modules/reputation/application/sync-reputation-from-event.ts:45— each module's own hardcodedevent.contractName !== '...'/=== '...'check.src/modules/{deliveries,escrow,fleet,disputes,reputation}/index.ts'sgetTrackedContracts()-equivalent wiring — each independently assigns the matching literal ascontractName: '...'.src/shared/queue/queues.ts:10-14— theQueueNameconst object, the project's own established pattern for exactly this kind of cross-file string constant, with its own header comment explaining the rationale.Impact
A single-character typo in any one of these seven-plus locations (e.g., renaming
'dispute-resolution'to'disputes-resolution'in the indexer's tracked-contract config without updating thedisputesmodule's own comparison) would silently and permanently stop that module from ever matching any event for that contract, with no compile-time error — the strings are compared as plain values, not checked against a shared literal type the compiler could flag a mismatch on.Expected behavior
Contract names are defined once, as a shared, typed constant every producer and consumer references — mirroring
QueueName.Proposed scope / implementation direction
ContractNameconst object (mirroringQueueName's shape) tosrc/shared/events/orsrc/blockchain/, with one entry per tracked contract (Escrow: 'escrow',Delivery: 'delivery',DisputeResolution: 'dispute-resolution',Fleet: 'fleet',IdentityReputation: 'identity-reputation').BlockchainEventEnvelope.contractNameasContractNameValuerather than a barestring, so a typo'd comparison anywhere becomes a compile-time type error rather than a silent runtime no-match.index.tstracked-contract wiring and each application-layer comparison) with the shared constant.Acceptance criteria
BlockchainEventEnvelope.contractNameis typed against that shared set, not a barestring.Verification / testing requirements
pnpm typecheckconfirms the narrowed type doesn't break any existing call site.