feat: scale improvements — O(1) reporting, Redis idempotency cache, centi-cent accrual#12
feat: scale improvements — O(1) reporting, Redis idempotency cache, centi-cent accrual#12
Conversation
Adds fractional_balance integer column to wallets (with non-negative CHECK) to support centi-cent royalty accrual. Adds ledger_totals table keyed by ledger_type for O(1) report lookups. Migration includes backfill from existing ledger rows.
Adds a @global() NestJS module that provisions a REDIS_CLIENT (ioredis) injection token, reading REDIS_HOST/REDIS_PORT from ConfigService. Used as the edge cache for idempotency checks in PurchasesService (Task 7). Also adds redis.module.ts to knip entry array so REDIS_CLIENT export is recognized as an infrastructure token, not dead code.
Short-circuits repeated requests by checking Redis before the DB query. On cache miss or Redis unavailability, falls through to the existing DB path.
Clear the 'processing' sentinel in the catch block of the DB transaction so clients can retry after failures (insufficient funds, DB errors, etc.) instead of receiving a permanent 409 for 24 hours.
- Skip royalty_author and royalty_platform ledger_totals upserts when the respective amount is 0, mirroring the existing ledger entry filter - Skip platform wallet UPDATE when platformCut is 0 to avoid spurious updatedAt touches when no money moves to the platform - Wrap JSON.parse of Redis cached value in try/catch; malformed values fall through to the DB path instead of throwing a 500 - Add comment explaining why all transaction errors clear the Redis sentinel (including business-rule failures like 402/403/404) - Fix reports.processor.spec.ts groupBy assertion from toBeUndefined() tautology to not.toHaveBeenCalled() — the old assertion always passed regardless of implementation
|
@coderabbitai review |
|
✅ Actions performedReview triggered.
|
WalkthroughThis pull request introduces a centi-cent fractional royalty accrual system, Redis-backed idempotency caching for purchases, and pre-aggregated financial reporting. New database schema includes a 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 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. Comment |
- RedisModule: implement OnModuleDestroy to close the ioredis client on
shutdown, preventing TCP connection leaks (matches DbModule pattern)
- PurchasesService: fix stale 'processing' sentinel and blind DEL bugs:
- track sentinelSet flag so only the request that set the sentinel
deletes it (prevents clobbering another request's cached result)
- wrap DB idempotency check in try/catch with conditional sentinel
cleanup so errors before the transaction don't leave stale sentinels
- skip sentinel DEL on PG_UNIQUE_VIOLATION (concurrent winner may have
already cached the completed-purchase JSON)
- add 3 targeted tests covering all three sentinel-ownership scenarios
- schema: add wallets_fractional_balance_lt_100 CHECK constraint and
generate migration 0002_stormy_sasquatch.sql
- deps: update @nestjs/core, @nestjs/common, @nestjs/platform-express,
@nestjs/swagger, @nestjs/testing to 11.1.18 and drizzle-orm to 0.45.2;
add pnpm overrides for handlebars, flatted, lodash, smol-toml,
serialize-javascript, multer, picomatch, and brace-expansion to
resolve all 30 moderate+ audit vulnerabilities
- knip: remove @nestjs/bullmq and bullmq from ignoreDependencies now
that updated packages resolve them correctly
Move the wallets_fractional_balance_lt_100 CHECK constraint from the separate migration (0002_stormy_sasquatch) into the original migration (0001_modern_rogue) alongside the fractional_balance column and its non-negative check. No functional change — both constraints are applied atomically in a single migration step now.
When redis.set fails after a successful DB commit or on a DB cold-start hit, the 'processing' sentinel was silently left in Redis for 24h. Subsequent idempotent replays would hit the stale sentinel and get a false 409 'still in flight' instead of the completed purchase result. Fix: in both catch blocks, delete the sentinel when sentinelSet=true so retries fall through to the DB path rather than hitting stale state.
Summary
ledger GROUP BYscan with an O(1)ledger_totalsmaterialized table. Running totals are maintained transactionally on every deposit and purchase, so report generation is a single 4-row SELECT regardless of ledger size.RedisModule(ioredis) and a Redis SETNX edge cache inPurchasesService. New requests set a'processing'sentinel; completed purchases are cached as JSON. Redis unavailability falls through to the DB path without error. Sentinel is deleted on any transaction failure so clients can retry.wallets.fractional_balance(integer centi-cents) to eliminate systematic author under-payment. The 70% royalty remainder accumulates per-author and is swept into the integer balance whenever it crosses 100 centi-cents.ADRs
Test Plan
pnpm test— 57 unit tests passingpnpm typecheck— no errorspnpm lint— cleanpnpm db:migrate