Skip to content

feat: replace in-memory PrismaService fake with real PrismaClient (#475) - #536

Closed
mayborn005 wants to merge 68 commits into
JSE-ORG:mainfrom
mayborn005:dev
Closed

feat: replace in-memory PrismaService fake with real PrismaClient (#475)#536
mayborn005 wants to merge 68 commits into
JSE-ORG:mainfrom
mayborn005:dev

Conversation

@mayborn005

Copy link
Copy Markdown
Contributor

Summary

Replaces the 1532-line in-memory PrismaService fake with a real PrismaClient using @prisma/adapter-pg (Prisma v7 driver adapter). All Map-based stores removed — queries go directly to PostgreSQL.

Changes

Core

  • src/prisma/prisma.service.ts — extends PrismaClient instead of in-memory Maps
    • Constructor creates PrismaPg adapter internally, no datasources needed
    • onModuleInit()$connect() + slow-query logger via $on(query)
    • onModuleDestroy()$disconnect()
    • reset()TRUNCATE TABLE ... CASCADE (skips _prisma_migrations)
    • All 40+ custom type exports preserved for backward compat

Behavioral Fixes

  • src/escrow/buyer-dispute.service.tsopenDispute() now explicitly calls escrowRepository.updateState(escrowId, DISPUTED) after creating a dispute (in-memory fake auto-transitioned escrow as side-effect; real DB does not)
  • test/integration/vendor-analytics.integration-spec.ts — removed (prisma as any).escrows.set(...); passes createdAt directly to prisma.escrow.create()

Test Updates

  • src/prisma/prisma.service.spec.ts — updated for real PrismaClient
  • src/prisma/escrow-event-logging.spec.ts — updated for real PrismaClient
  • test/unit/prisma.service.spec.ts — updated for real PrismaClient
  • test/unit/prisma-schema-parity.spec.ts — updated for real PrismaClient

CI

  • .github/workflows/ci.yml — added Postgres 16 service (matched from test.yml)

Known Behavioral Changes

  • findMany() no longer auto-filters CANCELLED records
  • escrow.create()/escrow.update() no longer auto-create EscrowEvent rows
  • dispute.create() no longer auto-transitions escrow to DISPUTED
  • amount fields are Prisma.Decimal at runtime — use toEqual() instead of toBe() for comparisons, or Number(escrow.amount) for arithmetic

Testing

Requires PostgreSQL on localhost:5432 with trustlink_test database. See AGENTS.md for setup.

Rickyy1017 and others added 24 commits June 25, 2026 20:06
…ils (JSE-ORG#437)

* fix(escrow): read event history from EscrowEvent table

- Update findEvents to read from EscrowEvent table instead of deriving from timestamp columns
- Add fromState and toState fields to EscrowEventEntry type
- Update Swagger documentation for the events endpoint
- Add tests for event history functionality

Closes JSE-ORG#425

* fix(seed): make seeding idempotent and fail loudly

- Use findFirst and create instead of upsert for idempotency
- Add tests for seed script
- Log summary of created records

Closes JSE-ORG#424

* fix(logistics): remove unused Terminal Africa provider

- Remove terminal-africa.provider.ts as it has no HTTP client and is not registered
- Remove corresponding test file

Closes JSE-ORG#423

* feat(vendor): implement VendorAccountDetails CRUD

- Add vendorAccountDetails store to PrismaService
- Add repository, service, and controller for VendorAccountDetails
- Add DTOs with validation and response masking
- Mask sensitive fields (bank account, tax ID) in responses
- Add tests for repository operations

Closes JSE-ORG#422

* style: format code with prettier
…SE-ORG#450)

- Replace pattern-only validation with Keypair.fromSecret/fromPublicKey
- Catch checksum-invalid keys at config load time, not runtime
- Add stellarSecretKey and stellarPublicKey custom Joi validators
- SYSTEM_SIGNER_SECRET, SEP10_SIGNING_SECRET now validated with checksum
- ADMIN_ADDRESS now validated as public key with checksum
- Error messages name the field and explain checksum failure
- Set abortEarly: false to report all validation errors at once
- Add comprehensive test suite: 38 tests covering valid/invalid keys
- Tests verify shape-valid but checksum-invalid keys are rejected
- Tests verify public keys rejected where secret keys expected
- Tests verify error messages name field and say 'invalid' not 'pattern'

Closes JSE-ORG#420

Co-authored-by: Alu-card19 <clintoncodes68@gmail.com>
…ts isolated (JSE-ORG#483)

* fixed: all issues resolved JSE-ORG#315 JSE-ORG#316 JSE-ORG#317 JSE-ORG#318

* fix: prisma middleware and notification retry queue JSE-ORG#315 JSE-ORG#316 JSE-ORG#317 JSE-ORG#318

* fix: exponential backoff jitter, retry logging, slow query middleware, prisma timeouts JSE-ORG#315 JSE-ORG#316 JSE-ORG#317 JSE-ORG#318

* fix(config): validate AUTO_RELEASE_SOURCE_ADDRESS and keep config tests isolated

---------

Co-authored-by: eric <ericjo6303@gmail.com>
…G#449)

Webhook signature verification was silently skipped when
STELLAR_WEBHOOK_SECRET was not configured, allowing unverified
requests to be processed. This fix ensures all incoming webhook
requests are either verified or rejected.

Changes:
- verifySignature() throws InternalServerErrorException (500)
  when secret is missing instead of silently accepting
- Logs an explicit configuration error with distinct message
  (stellar.webhook.secret_missing)
- Config validation requires STELLAR_WEBHOOK_SECRET in production
  using Joi.when (follows existing SENTRY_DSN pattern)
- All unit/integration tests now use proper secrets and signatures
- .env.example updated to document rejection behavior

Closes JSE-ORG#421
…ORG#451)

The docs/events.md document described canonical event topics as a
one-element tuple (Symbol("<event_name>"),) but the escrow contract
emits two-element tuples (symbol_short!("<Category>"), symbol_short!("<Action>"))
for all canonical events, with an optional third Address element.

Key changes:
- Updated Encoding Rules to describe the two-symbol topic convention
- Replaced the event index table with real topic pairs verified from
  the contract source at trust-link-contract@4ffc37b
- Added explicit "Events Using a Different Convention" section for
  single-symbol exceptions (resolver_vote_recorded, contract_upgraded,
  storage_migrated)
- Explained how indexers derive event names from topic pairs
- Removed fees_withdrawn (not emitted by the contract)
- Updated FeeCollectorUpdated from legacy inline to proper struct
- Added the missing EscrowExpired event
- Updated section headings to match PascalCase struct names
- Added source verification note with commit hash and repository link

Verification: Every topic was checked against emitter calls in
contracts/escrow/src/events.rs.

Closes JSE-ORG#412

Co-authored-by: belloaliyu11 <belloaliyu11@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…time FIXED (JSE-ORG#482)

Co-authored-by: Kappa16 <anagbogut@gmail.com>
…ed data FIXED (JSE-ORG#486)

Co-authored-by: Kappa16 <anagbogut@gmail.com>
* implemented find Vendor Escrows loads

* implemented Eight stale Dependabot pull requests and no dependency update policy

* docs(api): declare response types on vendor and admin endpoints

* feat(health): split liveness and readiness probes
)

PATCH /admin/credentials/logistics ignored dto.key whenever a key was
already set, silently re-encrypting the existing (possibly compromised)
secret instead. The endpoint now always encrypts and stores the
submitted key via LogisticsService.rotateApiKey().

Closes JSE-ORG#498
…ource via config, allow Idempotency-Key in CORS (JSE-ORG#527)

- Remove PrismaService.reset() from the onModuleDestroy shutdown hook and
  guard it to refuse running outside NODE_ENV=test, so it can no longer
  truncate a production database on graceful shutdown.
- Remove the duplicate @Cron nonce cleanup from Sep10Service; NonceCleanupService
  is now the single scheduled job that deletes expired nonces.
- Resolve AUTO_RELEASE_SOURCE_ADDRESS through ConfigService on use in
  AutoReleaseWorker and AutoReleaseService instead of falling back to a
  fabricated Stellar address read from process.env at import time.
- Add Idempotency-Key to the CORS allowedHeaders list so browser clients on
  an allowed origin can call POST /escrow, which requires that header.

Fixes JSE-ORG#497
Fixes JSE-ORG#500
Fixes JSE-ORG#501
Fixes JSE-ORG#509
* fix(config): update ADMIN_ADDRESS and AUTO_RELEASE_SOURCE_ADDRESS validation schemas to validate Stellar public keys

* style(lint): run eslint autofixes across src and test/unit directories

* fix(escrow): correct escrow creation default state to CREATED and trigger notifications on webhook payments
* test(sanitization): increase contact encryption coverage above 85% (JSE-ORG#408)\n\nAdd comprehensive unit tests for contact encryption utility covering round-trip, format, IV randomization, tamper detection, and key validation.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test(escrow): reach >75% function coverage for EscrowService (JSE-ORG#409)\n\nAdd tests for cancelEscrow/cancelPendingEscrow auth branches, terminal-state rejections, updateBuyerContact terminal rejection, viewer flags, and privacy check for toPublicEscrow.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test(admin/escrow/prisma): raise coverage for ApiKeysController and BuyerDisputeService; expand PrismaService tests (JSE-ORG#410,JSE-ORG#411)\n\nAdd ApiKeysController endpoint tests for admin rejection, rotation branches, and secret leakage checks. Add comprehensive PrismaService in-memory store tests covering create/findUnique/findMany/update/updateMany/reset and security assertions.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test(admin/escrow): raise coverage for ApiKeysController and BuyerDisputeService (JSE-ORG#410)\n\nAdd ApiKeysController endpoint tests for admin rejection and secret leakage checks.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test(prisma): increase PrismaService line/function coverage above 80% (JSE-ORG#411)\n\nAdd comprehensive unit tests exercising PrismaService in-memory stores, reset(), updateMany(), and security assertions.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <copilot@example.com>
…ders (JSE-ORG#534)

* fix(security): stop overriding helmet headers

SecurityMiddleware no longer duplicates headers already managed by helmet (X-Content-Type-Options, X-Frame-Options, Strict-Transport-Security, X-XSS-Protection, Referrer-Policy, Permissions-Policy). Only Cache-Control for authenticated requests remains. HSTS is configured through helmet so it can be turned off outside production.

Closes JSE-ORG#508

* fix: add missing Optional import in PrismaService

The @optional() decorator was used without importing Optional from @nestjs/common.

* fix: use isProduction variable consistently and update integration test

Replaced duplicate configService.isProduction() call with the isProduction variable. Updated integration test to not expect helmet-managed headers.
…E-ORG#475)

- Rewrite PrismaService to extend PrismaClient with @prisma/adapter-pg
- Remove all 13 Map-based in-memory stores (1532 -> 420 lines)
- reset() now executes TRUNCATE TABLE ... CASCADE
- Add Postgres 16 service to CI workflow
- Fix BuyerDisputeService to explicitly transition escrow to DISPUTED
- Fix vendor-analytics integration test (remove direct store access)
- Update 4 test files for real PrismaClient API
@mayborn005
mayborn005 requested a review from Omoboi-dev as a code owner July 29, 2026 08:46
@drips-wave

drips-wave Bot commented Jul 29, 2026

Copy link
Copy Markdown

Hey @mayborn005! 👋 It looks like this PR isn't linked to any issue.

If this PR is for one of the issues assigned to you as part of a Wave, please link it to ensure your contribution is tracked properly. You can do this by adding a keyword to the PR description (e.g., Closes #123), or by clicking a button below:

Issue Title
#475 PrismaService is an in-memory fake; replace it with a real PrismaClient Link to this issue

ℹ️ Learn more about linking PRs to issues

iexwr and others added 4 commits July 29, 2026 12:11
- Add periodic sweep timer (60s interval, unref'd) that removes expired
  entries without requiring a get() call for that key.
- Enforce a bounded maximum size (MAX_MEMORY_ENTRIES = 1_000). When the
  map exceeds capacity the soonest-to-expire entries are evicted first.
- onModuleDestroy now clears the map and stops the sweep timer to
  prevent open handles in tests and graceful shutdown.
- 6 new tests: sweep removes expired keys, sweep preserves valid keys,
  bounded capacity eviction, unbounded growth prevention, onModuleDestroy
  clears the map.

Closes JSE-ORG#506
* ci: build Docker image on PR

* refactor(tests): type global exception filter test mocks

* feat(stellar): poll Soroban for events with two-symbol topic names
* refactor(admin): make the admin authorisation rule single and explicit

* feat(dlq): paginate the failed transaction listing

* docs(testing): document the three test suites and add test match validator

* test(coverage): gate branch and function coverage alongside lines

* test(dlq): update unit test mock for pagination count

---------

Co-authored-by: DevNetlife <DevNetlife@users.noreply.github.com>
* fix(types): resolve 7 typecheck errors

* test(common): cover sanitize.util paths

* test(stellar): cover contract.service retry logic
Rickyy1017 and others added 28 commits July 29, 2026 15:08
…SorobanPollerService cursor advancement (JSE-ORG#583)

Four issues (JSE-ORG#551-JSE-ORG#554).

Closes JSE-ORG#551 — test/unit/prisma.service.spec.ts's plaintext-contact test

The test already had `expect(...).rejects.toThrow(...)` — but
`escrow.create` isn't an `async` function, so assertEncryptedContact's
throw happens synchronously during evaluation of the `create(...)`
expression, before it's ever handed to `expect()`. `.rejects` only catches
a *rejected promise*; a synchronous throw during argument evaluation
escapes expect() entirely. Fixed by wrapping each call in its own
`async () => { await create(...) }`, which guarantees a promise comes out
either way. Added a positive-case test that writes a properly encrypted
contact via encryptContact() and reads it back with decryptContact().

Closes JSE-ORG#552 — jest.mock('axios') erasing isAxiosError

A bare `jest.mock('axios')` auto-mocked the whole module, replacing
`axios.isAxiosError` with a jest.fn() returning undefined, so
GiglClient.fetchTracking's entire error-mapping branch was dead. Switched
to spreading the real module through the mock (keeping isAxiosError, a
real duck-typed predicate, genuine) while still mocking `create` per-test.
Needed the same `create` jest.fn() reference at both the top level and
under `default` — this codebase's esModuleInterop resolution didn't
consistently pick one over the other. Added the missing 401 case
(GiglUnauthorizedError) and a non-Axios-error-propagates-unchanged case.
gigl.client.ts branch coverage: 71.42% (was ~21%).

Closes JSE-ORG#553 — lint:check errors/warnings

Ran `npx eslint --fix` for the auto-fixable prettier errors, removed the
unused-variable fixtures in prisma.service.spec.ts / contract.service.spec.ts
by dropping their unused `const x =` bindings (not disabling the rule),
and fixed unused function params/destructured elements in
tracing.interceptor.spec.ts the same way. Two `no-require-imports` errors
in config.module.spec.ts are a deliberate dynamic re-require after
jest.resetModules() (a static import would be cached and never re-run the
module's load-time env validation) — disabled with a one-line reason per
the issue's own guideline, not silenced blindly.

Along the way, found and fixed the same duplicate-file-content corruption
JSE-ORG#550 already covers in escrow.service.spec.ts, in two more files:
tracing.interceptor.spec.ts and tracing.service.spec.ts each had a
near-duplicate second copy of their whole test suite appended (with a few
tests written differently) rather than a merge conflict properly resolved.
Kept the more complete version in each case and folded in the one
genuinely distinct test tracing.service.spec.ts's second copy had
("is disabled when OTEL_ENABLED is unset").

Remaining `npm run lint:check` errors (46) are explicitly out of scope:
config.module.ts (8) and analytics.service.ts (31) are `any`/unsafe-type
errors — the issue itself says "JSE-ORG#416 covers the remaining any occurrences
... keep the two from colliding by doing formatting here and typing
there." dlq.service.ts (6) is a real but unrelated type bug (its list()
passes skip/take/count to an in-memory Prisma stub whose type doesn't
declare them) that doesn't affect this PR's use of DlqService (enqueue()
only). api-keys.controller.spec.ts (1) is the parsing error JSE-ORG#547 already
fixes in a separate open PR.

Closes JSE-ORG#554 — SorobanPollerService cursor advancement past failed events

Chose stop-at-first-failure over continue-and-track-highest-contiguous.
The batch is fetched in ledger order and syncStateFromChain's switch cases
assume prior transitions already landed (EscrowShipped assumes
EscrowFunded already happened) — applying later events out of order after
skipping a failed one risks a worse, harder-to-detect state-machine bug
than the at-least-once redelivery this trades for. Full reasoning is in
poll()'s doc comment.

Two distinct failure classes, handled differently (see processEvent's doc
comment):

  - Legitimately not applicable (unparseable payload, non-string topics,
    missing escrowId) — no retry ever fixes this, so it's dead-lettered
    via the existing DlqService (issue JSE-ORG#303, reused rather than inventing
    a second mechanism per the issue's own guidance) and treated as
    handled; the cursor advances past it.
  - syncStateFromChain throws — rethrown so poll() stops the batch and
    retries this exact event next cycle. A per-event-id in-memory retry
    counter (MAX_SYNC_RETRIES = 5) breaks the infinite-retry case: once
    exceeded, the event is also dead-lettered and the cursor advances.

Wired DlqService into StellarModule via forwardRef (DlqModule already
imports StellarModule for the replay path, so this closes a module cycle
the same way WebhooksModule/EscrowModule already do in this file).

New test/unit/soroban-poller.service.spec.ts covers all three required
scenarios (all succeed, middle throws, first throws) plus dead-lettering
for unparseable payloads, missing escrowId, and the retry-threshold path.

Testing note: this sandbox has no Docker and a pre-existing (unrelated)
Homebrew permissions issue blocks a local Postgres install, so
`npm run test:e2e` and the e2e-spec files could not be run — none of these
four issues touch e2e files, though, so this is lower-risk than it was for
the previous PR. Everything here was verified via `npx tsc --noEmit -p .`
(clean for every touched file), `npx eslint` (clean for every touched
file), and `npx jest test/unit` (390/392 passing — the 2 failures are the
exact pre-existing dev-baseline bugs JSE-ORG#547/JSE-ORG#550 already fix in the other
open PR, untouched here to avoid duplicate/conflicting work across both).

Co-authored-by: miraclesonly <304894442+miraclesonly@users.noreply.github.com>
Co-authored-by: Omoboi-dev <bukunmiabiodun14@gmail.com>
…oped middleware, and config accessors

- SentryInterceptor: pins that a successful response passes through
  untouched, a thrown error is reported to Sentry and rethrown (not
  swallowed into a 200), and that the interceptor never reads the
  ExecutionContext at all — so no request body or header can reach the
  Sentry event through it.
- SanitizationPipe: covers flat/nested/array bodies, non-string values
  passing through unchanged, markup being stripped vs an ordinary
  value being left alone, and the documented in-place mutation.
- SecurityMiddleware/RequestIdMiddleware/LoggerMiddleware: covers the
  authenticated vs unauthenticated cache-header branches (100% branch
  coverage on security.middleware.ts), request-id generation vs an
  honoured inbound header vs an array header, and that the logger
  middleware calls next() and writes one structured JSON line on
  finish. Also documents that RequestIdMiddleware accepts an inbound
  id verbatim with no format validation before echoing it into logs
  (finding only, not fixed here).
- ConfigService: covers getAllowedOrigins (unset/empty/single/multiple/
  whitespace), isProduction/isDevelopment/isTest across all NODE_ENV
  values, and getDatabaseUrl's pool-parameter accessors (default,
  connection_limit only, pool_timeout only, both). Tests the service
  directly against a small fake NestConfigService rather than a
  compiled Nest module, per JSE-ORG#491.

Closes JSE-ORG#570
Closes JSE-ORG#571
Closes JSE-ORG#572
Closes JSE-ORG#573
The CORS origin callback in main.ts was constructing an Error for
disallowed origins, which the cors middleware passed to next(err).
This caused the global exception filter to return a 500 Internal
Server Error instead of a clean denial.

Change callback(new Error(...)) to callback(null, false), the
standard cors convention. This omits the Access-Control-Allow-Origin
header and lets the browser block the request naturally.

Adds cors-origin.spec.ts covering:
- Allowed origin receives ACAO header
- Disallowed origin gets no ACAO header and no 5xx
- Absent Origin header succeeds (non-browser clients)
- Preflight for allowed origin still advertises Idempotency-Key
- Preflight for disallowed origin omits ACAO header

Co-authored-by: KingFRANKHOOD <chibuezemfrancis@gmail.com>
* test(vendor): cover the untested paths in analytics.service.ts

- Add full getTransactionStats test suite: zeroed figures for vendor
  with no escrows, all active states (CREATED/FUNDED/SHIPPED/DELIVERED),
  COMPLETED + RELEASED counted as completedTransactions, exact
  completion/dispute rate arithmetic, channel metrics (no settings,
  EMAIL only, SMS only, both enabled), lastUpdated ISO timestamp
- Add vendor isolation tests for both getDailyVolumeChart and
  getTransactionStats — vendor B's escrows never appear in vendor A's totals
- Strengthen timezone boundary test: pins escrow to 00:30 UTC (always
  within the rolling window), asserts exact date attribution differs
  between UTC and America/New_York views
- Add formatDateInTimezone tests for UTC-5 (NY) and UTC+9 (Tokyo)
  with exact expected date strings
- All assertions use exact expected values (toBe), no toBeGreaterThan

Coverage: analytics.service.ts 60% → 98.33% lines, 100% functions.
Only uncovered line is 219 (cancelledTransactions branch) — unreachable
in unit tests because the in-memory PrismaService mock silently drops
CANCELLED escrows from findMany without an explicit state filter.
Production Postgres path is unaffected; integration test needed to
verify cancelled count end-to-end.

* test(vendor): cover the untested paths in analytics.service.ts

- Remove CANCELLED filter from PrismaService mock findMany; the filter
  was silently dropping CANCELLED escrows from getTransactionStats,
  making cancelledTransactions unreachable and giving vendors wrong
  revenue figures. Real Postgres has no such filter.

- Fix admin-stats.service.spec.ts to expect 9 escrows (including
  CANCELLED) now that the mock matches real Postgres behaviour.

- Add test: 'counts CANCELLED escrows in cancelledTransactions and
  totalTransactions' — covers the if (state === 'CANCELLED') branch
  (line 218) that was previously unreachable.

- Add test: 'uses default days=30 and timezone=UTC when called with
  vendor address only' — covers the default-arg branches for days
  and timezone in getDailyVolumeChart.

- Remove stale comment in mixed-states test that documented the now-
  fixed mock quirk.

Coverage for analytics.service.ts:
  Lines:      98.33% → 100%
  Branches:   74.07% → 85.18%
  Functions:  100%   → 100%
  Statements: 98.43% → 100%

---------

Co-authored-by: Omoboi-dev <bukunmiabiodun14@gmail.com>
…call fails (JSE-ORG#594)

* fix(workers): make delivery recording recoverable when contract call fails (JSE-ORG#507)

Swapped the order of operations in TrackingPollWorker.run:
contractService.recordDelivery is now called before
escrowRepository.markDelivered. If the chain call fails,
the escrow remains in SHIPPED state and will be retried
on the next poll cycle.

Added claimDelivery/clearDeliveryClaim to EscrowRepository
following the same claim-and-release pattern used by
AutoReleaseWorker.

Closes JSE-ORG#507

* style: fix prettier formatting in tracking-poll intervals spec

---------

Co-authored-by: Buffy <buffyg@freebuff.com>
…SE-ORG#446)

* test(notifications): cover backoff computation and retry exhaustion

* quick fix [ci skip]

* chore: remove committed coverage report and restore package.json/package-lock.json from main

* fix: add cross-env dep, restore test scripts flag, fix retry queue service merge error

---------

Co-authored-by: OluRemiFour <lekanayoola04@gmail.com>
…ally run (JSE-ORG#491) (JSE-ORG#526)

The 27 not-Keypair tests in config.module.spec.ts could never reject
anything. NestConfigModule.forRoot({ validationSchema }) is invoked
inside ConfigModule's @module decorator, so @nestjs/config loads the
env file and runs Joi validation once when config.module.ts is first
imported. Each subsequent buildConfigService(env) call returned the
already-validated config from .env.test, regardless of the env the
test passed. The 11 passing tests were all the Keypair SDK unit tests,
which don't depend on the Nest module lifecycle.

Fix: extract the schema and the two custom validators (stellarSecretKey,
stellarPublicKey) into src/config/config.schema.ts so the runtime path
and the spec share one definition. Call schema.validate(env, { abortEarly:
false, allowUnknown: true }) directly per test, asserting on
result.error (instead of a second Nest module compile). Runs Joi
synchronously, so the rewrite drops the async/await on the configure-
module branch.

Side benefit: the original config.module.ts defined stellarPublicKey but
never wired it onto ADMIN_ADDRESS, which fell through as plain
Joi.string().required(). ADMIN_ADDRESS now actually validates as a
Stellar public key (checksum + G prefix) at boot. Tests like 'ADMIN_ADDRESS
rejects a secret key (S...)' now reject in prod, not just in this spec.

Validators also override .messages({'any.invalid': '{{#label}} {{#reason}}'})
so error.message carries the full customised reason instead of Jiang's
default "contains an invalid value".

Acceptance criteria from JSE-ORG#491:
- All 38 tests in src/config/config.module.spec.ts pass.
- Each test genuinely exercises the value it supplies (result.error
  matches what the per-test env produced — flipping a fixture from a
  valid key to an invalid one flips the test result).
- The checksum cases cover both SYSTEM_SIGNER_SECRET and SEP10_SIGNING_SECRET,
  both the public-key-where-a-secret-is-expected case, and ADMIN_ADDRESS.

Pre-existing failures left untouched (verified against clean dev HEAD
via git stash):
- 11 unrelated test suites fail with module-resolution errors
  (missing .prisma/client/default, broken DTO paths). Owned by other
  issues.
- One typecheck error in
  src/notifications/notification-retry-queue.service.ts(204,5). Owned
  by JSE-ORG#488.

Closes JSE-ORG#491

Co-authored-by: MrOmale <MrOmale@users.noreply.github.com>
Co-authored-by: Omoboi-dev <bukunmiabiodun14@gmail.com>
* fix(config): resolve process.env test isolation and custom Joi validation messages (JSE-ORG#481)

Signed-off-by: namdamdoi68-oss <namdamdoi68@gmail.com>

* fix(test): use checksum-valid Stellar pubkey for ADMIN_ADDRESS in .env.test

src/config/config.module.ts's production stellarPublicKey validator (introduced when the Joi schema was tightened to use Keypair.fromPublicKey for checksum validation, era of commit 62f7137) rejects the literal "admin-address" placeholder that .env.test has carried since the repo's inception as ADMIN_ADDRESS. The result is that `npm run test:cov` aborts mid-suite with:

    Error: Config validation error: ADMIN_ADDRESS is an invalid Stellar public key \
      — must start with G, got a value starting with 'a'

Replace the placeholder with the testnet-format pubkey GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5 — the same value already declared as VALID_PUBLIC_KEY in src/config/config.module.spec.ts (line 21). It is checksum-valid, which is the only property the production validator checks beyond the G-prefix, and it is unambiguously test-only data.

The lone test that depends on the OLD placeholder literal — test/unit/dispute.service.spec.ts:176 — uses it inside a Jest mock (`get: jest.fn().mockReturnValue('admin-address')`), not by reading process.env, so it is unaffected. No production code references the literal. AGENTS.md documented this as fixed earlier under "Stellar addresses" but .env.test was never re-synced with the validator change.

Unblocks `npm run test:cov` end-to-end. Project total coverage now 77.14% lines (above 70% gate).

* test(logistics): cover GiglClient error paths

Closes JSE-ORG#404.

Issue JSE-ORG#404 reports GiglClient at 46% line / 0% branch coverage with zero tests for any of its error-handling branches: every axios failure was leaking upward instead of being translated to a typed error. This commit exercises every branch of fetchTracking and adds a small shape-validation guard so a malformed 2xx body is rejected rather than passed through.

## Implementation (src/logistics/gigl/gigl.client.ts)

- New: GiglInvalidResponseError — fourth typed error in the class hierarchy (alongside GiglUnauthorizedError, GiglNetworkError, GiglProviderError). Thrown when a 2xx response body fails GiglTrackingResponse shape validation.
- New: isValidGiglResponse type guard — checks that response.data is a non-null object with string tracking_number, string current_status, string carrier_code, string|null estimated_delivery, and Array.isArray(events).
- fetchTracking now invokes the guard inside the try block after the await. The validation error is re-thrown verbatim by the existing non-axios branch in the catch, so no new control flow is required.
- @throws JSDoc updated to reflect the new failure mode.

## Tests (src/logistics/gigl/gigl.client.spec.ts)

28 new tests organised in six describe blocks. All branches exercised:

- constructor — axios.create configuration (default 10s timeout, custom timeoutMs, bearer-token assembly verbatim).
- fetchTracking — happy path (URL encoding, payload passthrough, null estimated_delivery).
- fetchTracking — network-level failures (ECONNABORTED, ETIMEDOUT, ECONNREFUSED with axiosErr.message used as cause, ECONNRESET with null message exercising the `?? 'unknown network error'` fallback).
- fetchTracking — HTTP error status (401 -> GiglUnauthorizedError, 404/500/503 -> GiglProviderError with statusCode field, plus a parallel-call test that asserts 404 and 500 are distinct shape-wise on the same client).
- fetchTracking — malformed response bodies (null body, partial body, wrong-type estimated_delivery, array body, missing events, missing tracking_number, plus an affirmative that an axios network error does NOT spuriously trip GiglInvalidResponseError).
- fetchTracking — non-axios errors (Error instance re-thrown verbatim by identity; non-Error primitive re-thrown verbatim).

Error-class structural assertions cover name/extends/inheritance for all four typed error classes.

Mock strategy: jest.mock('axios') auto-mock; a plain-object makeAxiosError helper returns `{ isAxiosError: true, code, message, response }` — satisfies real axios.isAxiosError contract and lets `message: null` exercises survive the test run (true Error instances coerce null message back to string in some engines). No real network request is made.

## Tests (src/logistics/gigl/gigl-logistics.service.spec.ts)

+2 propagation tests pinning that GiglInvalidResponseError flows through getStatus() and getTrackingDetails() unchanged. The service does no transformation of fetchTracking's rejection, so this is a contract test rather than a behavioural test; it documents the dependency so a future refactor that wraps the rejection can't silently regress it.

## CHANGELOG

Unreleased > Changed: documents the malformed-body rejection behaviour for callers that consume the GIGL provider strictly.

## Coverage

| file                                            | lines (before -> after) | branches (before -> after) |
|-------------------------------------------------|-------------------------|----------------------------|
| src/logistics/gigl/gigl.client.ts               | 46.15% -> 100.00%       | 0.00%  -> 100.00%          |
| src/logistics/gigl/gigl-logistics.service.ts    | unchanged               | 91.66%                     |
| project total (line-coverage gate)              |  70%-floor preserved    | 77.14% lines (> 70% floor) |

Suites: 75 passed. Tests: 705 passed (was 642). No real network requests are made in unit runs. AGENTS.md earlier claim of "all 65 suites passing" was based on a stale coverage summary; the actual test count after this PR is 75 suites / 705 tests.

## Acceptance criteria (from issue JSE-ORG#404)

- [x] Line and branch coverage above 80% (100% / 100%)
- [x] axios instance configured with base URL, bearer token and timeout (3 constructor tests)
- [x] Successful response returned as the typed tracking shape (3 happy-path tests)
- [x] Network timeout produces the typed network error, not a raw axios error (4 network-error tests including the `?? 'unknown network error'` fallback)
- [x] HTTP 404 surfaced distinctly from HTTP 500 (per-statusCode + per-message assertions; parallel-call test)
- [x] Malformed response body is rejected rather than returned as-is (6 invalid-shape tests)
- [x] No test makes a real network request (jest.mock('axios'))
- [x] `npm run test:cov` passes and overall coverage stays at or above 70% (77.14%)

---------

Signed-off-by: namdamdoi68-oss <namdamdoi68@gmail.com>
Co-authored-by: namdamdoi68-oss <namdamdoi68@gmail.com>
Co-authored-by: Omoboi-dev <bukunmiabiodun14@gmail.com>
* test(vendor): add comprehensive controller tests for VendorProfileController (JSE-ORG#461)

* test(escrow): add comprehensive controller tests for EscrowController (JSE-ORG#459)
* test(vendor): add comprehensive controller tests for VendorProfileController (JSE-ORG#461)

* test(escrow): add comprehensive controller tests for EscrowController (JSE-ORG#459)

* feat(stellar): replace placeholder contract calls with real Soroban contract invocations (JSE-ORG#478)

- Update StellarModule to configure a real Soroban rpc.Server using ConfigService.
- Implement full Soroban transaction lifecycle (account fetch, transaction construction, simulation, preparation, signing, submission, polling) in ContractService using @stellar/stellar-sdk.
- Preserve submitAutoRelease sequence retry semantics and re-fetch account on each attempt.
- Add typed simulation and contract error decoding.
- Expand contract.service unit tests covering full RPC lifecycle and edge cases.

---------

Co-authored-by: Omoboi-dev <bukunmiabiodun14@gmail.com>
…nterval, request timeout, retention-safe start ledger (JSE-ORG#588)

* fix(soroban): require an explicit RPC URL in production instead of guessing

resolveRpcUrl() silently fell back to the public testnet RPC (or a
third-party mainnet endpoint that needs an API key) when SOROBAN_RPC_URL
was unset, so a misconfigured production service could read testnet
events and apply them to production escrows.

- Require SOROBAN_RPC_URL (as a URI) in production and whenever
  STELLAR_NETWORK is MAINNET; startup now fails fast instead of guessing.
- Reject a testnet URL under STELLAR_NETWORK=MAINNET and vice versa, so
  the RPC endpoint and the configured network always agree.
- Remove the third-party mainnet fallback entirely.
- Outside production, keep the public testnet default but log clearly
  that the poller is running on a default.
- Fix the onModuleInit guard and warning to name only CONTRACT_ID, the
  one condition that can actually disable the poller.
- Export the Joi schema so config shapes can be tested without booting
  Nest, and cover the new rules with tests.

* fix(config): validate SOROBAN_POLL_INTERVAL_MS as a bounded integer

The poll interval was read with Number() and no schema entry, so a
non-numeric value became NaN and setInterval(fn, NaN) fired as fast as
the event loop allows, hammering the Soroban RPC endpoint. A value of 0
behaved the same way.

- Declare SOROBAN_POLL_INTERVAL_MS in the Joi schema as an integer with
  a 1000ms minimum and a default of 5000, with clear startup errors.
- Drop the ternary fallback in the poller constructor so the schema is
  the single source of truth for the default.
- Mark the value as always present in the Config interface.
- Document the bounds in .env.example and add tests asserting that
  'abc', 0, sub-minimum, and non-integer values all fail validation and
  that the default applies when unset.

* fix(soroban): bound the RPC request with an AbortController timeout

fetchEvents called fetch with no signal, so a single request that never
settled left the polling guard set forever: every later tick returned
at the guard, the process stayed healthy, and event ingestion silently
stopped with nothing in the logs.

- Route all RPC calls through a helper that follows the checkHorizon
  pattern: AbortController, setTimeout to abort, clearTimeout in
  finally.
- Make the timeout configurable via SOROBAN_RPC_TIMEOUT_MS (validated
  integer, default 4000ms — below the 5000ms poll interval).
- Log a timeout distinctly (warn) from other poll failures; the next
  tick proceeds normally because the abort settles the request and the
  finally block always clears the polling flag.
- Add tests asserting a request that never resolves is aborted, logged
  as a timeout, and does not block the following cycle.

* fix(soroban): start from the current ledger when no cursor is stored

With no cursor, getEvents was called with startLedger: 1. Soroban RPC
nodes retain only a short window of events (~24h), so the node rejects
the request, poll() logs the error, and the cycle repeats every tick —
a fresh deployment never ingested a single event.

- On first run, ask getLatestLedger for the current ledger and start a
  small margin behind it, keeping the request inside the retention
  window. Events emitted before the first deployment are intentionally
  never read; this is now documented rather than accidental.
- Persist the chosen start ledger immediately (as a 'ledger:'-prefixed
  cursor value) so a restart resumes from the same point instead of
  skipping forward to a new "now". Stored paging tokens keep working
  unchanged.
- Add SOROBAN_START_LEDGER so an operator can replay from a known
  ledger after an outage.
- Distinguish a retention-window error from other RPC errors in the
  logs.
- Add tests asserting the request body for the no-cursor, paging-token,
  ledger-cursor, and configured-start-ledger cases, plus the error
  classification.

---------

Co-authored-by: distributed-nerd <267643428+distributed-nerd@users.noreply.github.com>
Co-authored-by: Omoboi-dev <bukunmiabiodun14@gmail.com>
…retries (JSE-ORG#590)

Adds coverage for the per-attempt failure persistence (retryCount,
failedAt, lastError) that operators rely on to see the attempt count
and provider error while a notification is retrying.

- After three failing attempts the notification row shows retryCount: 3
  and the last provider error message as lastError.
- Every failing attempt writes retryCount/failedAt/lastError, not just
  the final one.
- A rejecting DB write does not abort the retry loop (it is caught and
  logged) and the terminal status: 'FAILED' write still happens.

closes JSE-ORG#490

Co-authored-by: Jerry_tekh <jerry.chinonso134@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Omoboi-dev <bukunmiabiodun14@gmail.com>
Adds a typecheck job to lint.yml so a non-compiling file can no longer
reach dev through a green pull request, and adds controller-level tests
for three previously untested controllers: StressTestController,
VendorEscrowController, and StellarWebhookController.

Closes JSE-ORG#577
Closes JSE-ORG#576
Closes JSE-ORG#575
Closes JSE-ORG#574
…SE-ORG#603)

* fix: resolve bad merge in stellar.module.ts causing build errors

* fix: add DATABASE_URL build arg so prisma generate runs in Docker build

---------

Co-authored-by: Buffy <buffyg@freebuff.com>
…es (JSE-ORG#602)

* fix: regenerate package-lock.json to resolve @emnapi version mismatches

* fix: add DATABASE_URL build arg so prisma generate runs in Docker build

---------

Co-authored-by: Buffy <buffyg@freebuff.com>
* test(tracing): improve coverage for tracing service and bootstrap

* fix(prisma): restore optional database url injection
…ead of loading all rows (JSE-ORG#596)

* fix: resolve CI conflicts for dispute pagination and admin stats aggregates

* fix: regenerate package-lock.json to resolve @emnapi version mismatches

* fix: resolve pre-existing test failures in escrow.service.spec.ts and dispute.repository.spec.ts

* fix: repair Docker build - add DATABASE_URL arg, fix stellar.module merge, fix type casts

* fix: resolve all pre-existing build errors - config.module, dlq, logistics, analytics, readiness dto

* fix: suppress lint errors from as any casts, fix escrow-event-logging test state

---------

Co-authored-by: Buffy <buffyg@freebuff.com>
…f in-memory (JSE-ORG#595)

* fix: resolve CI conflicts for dispute pagination and admin stats aggregates

* fix: regenerate package-lock.json to resolve @emnapi version mismatches

* fix: resolve pre-existing test failures in escrow.service.spec.ts and dispute.repository.spec.ts

* fix: repair Docker build - add DATABASE_URL arg, fix stellar.module merge, fix type casts

* fix: resolve all pre-existing build errors - config.module, dlq, logistics, analytics, readiness dto

* fix: suppress lint errors from as any casts, fix escrow-event-logging test state

---------

Co-authored-by: Buffy <buffyg@freebuff.com>
Bring the real-PrismaClient PR up to date with base dev (~40 commits).

Conflict resolutions:
- src/prisma/prisma.service.ts: keep the PR's real PrismaClient (drop the
  in-memory fake).
- test/unit/prisma.service.spec.ts, src/prisma/escrow-event-logging.spec.ts:
  keep the PR's real-DB-oriented tests. Upstream's added assertions targeted
  fake-only behaviour (sync throw on plaintext contact, auto-DISPUTED side
  effects, notifications without a parent escrow, no-arg effectiveDatabaseUrl
  undefined) and do not hold against a real PrismaClient.
- .github/workflows/ci.yml: accept upstream's deletion (its coverage/typecheck
  work was consolidated into test.yml in JSE-ORG#525).

Note: this merge is textually clean but not yet green. The fake->real swap
surfaces ~151 type errors in code the base added against the fake's loose
types (Decimal/JsonValue/enums/required itemRef), and @prisma/adapter-pg + pg
still need to be added to package.json. Those are follow-up commits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HzXa5DbrJ4CsnYs4Sx4qiK
Reconcile the fake->real PrismaClient swap with ~40 commits of base code
that was written against the in-memory fake's loose types.

- Declare the previously-undeclared driver-adapter deps so `npm ci` installs
  them: @prisma/adapter-pg, pg (deps) and @types/pg (devDep).
- prisma.service.ts: enable query-event logging via constructor `log` option
  and narrow $on('query') to Prisma.QueryEvent; drop dead input types; add
  boundary mappers (toEscrowRecord/toFailedTransactionRecord/
  toVendorAccountDetailsRecord/toVendorTrackingSettingsRecord) that convert
  generated rows (Decimal, JsonValue) to the hand-written *Record contract
  (number, plain objects) the rest of the app and its tests depend on.
- escrow / dlq / vendor repositories: convert rows through the mappers;
  write JSON columns via Prisma.DbNull / InputJsonValue.
- admin-stats: Number() the Decimal _sum.amount aggregate.
- analytics: type $queryRaw result as an array.
- schema: add Notification.providerMessageId / attemptCount / lastResponseCode
  (persisted by NotificationsService; previously only in the fake) + migration.

Production `tsc --noEmit` is clean. Remaining type errors are confined to
test/spec files and the benchmark script (follow-up commit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HzXa5DbrJ4CsnYs4Sx4qiK
…pecs

Make `npm run typecheck` (tsc --noEmit) fully green after the fake->real
PrismaClient migration. Type-only test changes plus repair of pre-existing
bad-merge corruption inherited from base dev.

- Add required `itemRef` to escrow creates; annotate `state: '...' as const`
  where widening broke EscrowRecord/EscrowSummaryDto assignability
  (escrow/dispute/admin-stats/analytics/cross-vendor/tracking-poll specs).
- Add missing fields to VendorTrackingSettingsRecord mocks; cast the DLQ
  ABANDONED record; pass the new DlqService arg to SorobanPollerService;
  use Prisma.DbNull for null JSON columns.
- prisma.service.spec.ts: drop the invalid `@jest/globals` Test import, remove
  unused e1..e4 bindings, Prisma.DbNull for ledgerFeedback.
- Remove duplicated halves left by earlier bad merges in
  tracing.interceptor.spec, tracing.middleware.spec, and
  notification-retry-queue.service.spec.
- config.module.spec.ts: repair the corrupted abortEarly block, the three
  missing sync-test `});`, the duplicate Keypair import, and restore the
  dropped ALL_KNOWN_KEYS definition.
- benchmark script: array-typed $queryRaw, itemRef on seed escrows.

No assertions or test intent changed. Note: the repo-wide `lint:check` job was
already red on base dev (pre-existing prettier/require/console violations in
untouched files); the files changed here are formatted clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HzXa5DbrJ4CsnYs4Sx4qiK
@mayborn005 mayborn005 closed this Aug 5, 2026
@mayborn005

Copy link
Copy Markdown
Contributor Author

Closing this duplicate — it is the same head as #537 but targets main (default branch is dev) and is not linked to issue #475. Keeping #537 (dev→dev) as the canonical PR for #475.

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.