Skip to content

feat: repository pattern, push notifications, bulk CSV import, socket decoupling - #168

Merged
Tybravo merged 3 commits into
SwiftChainn:mainfrom
Adeolu01:feat/combined-repository-notifications-bulk-sockets
Aug 30, 2026
Merged

feat: repository pattern, push notifications, bulk CSV import, socket decoupling#168
Tybravo merged 3 commits into
SwiftChainn:mainfrom
Adeolu01:feat/combined-repository-notifications-bulk-sockets

Conversation

@Adeolu01

Copy link
Copy Markdown
Contributor

closes #118
closes #131
closes #132
closes #117

Summary

Four related backend changes in one PR, following the combined-PR format used
by #161. The repository layer lands first because the notification and
bulk-import features are built on top of it.

Repository pattern (#118)

Adds src/repositories/ with a generic BaseRepository over Mongoose plus
concrete Delivery, User, Escrow, NotificationPreference, Notification
and ChatMessage repositories. Services depend on the IRepository contract
rather than importing models directly.

Design points:

  • Malformed ObjectId strings resolve to null rather than throwing a
    Mongoose CastError, so a bad path parameter surfaces as a clean 404 rather
    than an unhandled 500.
  • Pagination clamps page and limit, preventing negative skips and unbounded
    reads from a hostile or buggy caller.
  • Status transitions assert the expected prior state inside the query filter,
    so two concurrent transitions cannot both succeed. The loser matches no
    document and is rejected.
  • findExistingTrackingNumbers resolves duplicate checks for a whole batch in
    one query rather than one query per row.

Scope note for reviewers: the codebase currently has two parallel Delivery
lineages (Delivery.ts and the DeliveryLegacy model in deliveryModel.ts),
along with duplicated service and route files. I refactored the canonical
services onto repositories rather than touching every duplicated lineage,
which would have produced a diff that conflicts with most in-flight PRs.
Happy to widen the scope if you would prefer that in a follow-up.

Push notifications (#131)

Provider-agnostic push transport (IPushProvider) with a Firebase Cloud
Messaging implementation over the HTTP v1 API. Authentication uses the
service-account flow: an RS256 JWT is signed with the service account key and
exchanged for a short-lived OAuth2 access token, cached until shortly before
expiry with single-flight fetching so concurrent sends do not each hit
Google's token endpoint.

No new runtime dependency: the existing axios client and Node's built-in
crypto module are used rather than pulling in firebase-admin.

NotificationService resolves per-user preferences, dispatches to registered
devices, and records every attempt including opt-out skips and provider
failures, so "why didn't I get notified?" is answerable from the database
rather than from application logs.

Behaviour worth calling out:

  • Registering a token already held by another account detaches it from the
    previous owner first. Without this, a handed-over or re-authenticated device
    would keep receiving the previous user's delivery updates.
  • Only permanent provider failures (UNREGISTERED, INVALID_ARGUMENT,
    SENDER_ID_MISMATCH) prune a device token. Transient failures such as
    QUOTA_EXCEEDED leave the registration intact so a rate limit does not
    silently unsubscribe a user.
  • Push failures never roll back a committed delivery status transition.

DeliveryService.updateStatus encodes the delivery state machine as an
inspectable map and fires notifications after the write commits, covering the
Pending -> In Progress -> Completed path from the issue.

Endpoints

All authenticated and scoped to the calling user. No handler accepts a user id
from the client, so one user cannot read or mutate another's preferences.

Method Path Purpose
GET /api/v1/notifications Paginated notification history
GET /api/v1/notifications/preferences Read preferences (defaults created on first access)
PATCH /api/v1/notifications/preferences Enable/disable push, choose events
POST /api/v1/notifications/devices Register or refresh a device token
DELETE /api/v1/notifications/devices Remove a device token

Configuration

…et refactor

Implements four related backend changes. The repository layer lands first
because the notification and bulk-import features are built on top of it.

Repository pattern (SwiftChainn#118)

Adds src/repositories/ with a generic BaseRepository over Mongoose plus
concrete Delivery, User, Escrow, NotificationPreference, Notification and
ChatMessage repositories. Services depend on the IRepository contract
rather than on models directly.

Malformed ObjectId strings resolve to null instead of throwing a
CastError, so a bad path parameter surfaces as a 404 rather than a 500.
Pagination is clamped to prevent negative skips and unbounded reads.
Status transitions assert the expected prior state in the query filter,
so two concurrent transitions cannot both succeed.

Push notifications (SwiftChainn#131)

Adds a provider-agnostic push transport (IPushProvider) with a Firebase
Cloud Messaging implementation over the HTTP v1 API, authenticated by a
service-account JWT exchanged for a cached OAuth2 access token. No new
runtime dependency is introduced; the existing axios client and Node's
crypto module are used.

NotificationService resolves per-user preferences, dispatches to the
registered devices and records every attempt, including opt-out skips and
provider failures, so the notification history is answerable from the
database. Tokens the provider reports as permanently invalid are pruned.
Registering a token already held by another account detaches it from the
previous owner, so a handed-over device does not keep receiving the old
owner's deliveries.

DeliveryService.updateStatus encodes the delivery state machine and fires
notifications after the write commits. Push failures never roll back a
committed status transition.

Endpoints, all authenticated and scoped to the calling user:
  GET    /api/v1/notifications
  GET    /api/v1/notifications/preferences
  PATCH  /api/v1/notifications/preferences
  POST   /api/v1/notifications/devices
  DELETE /api/v1/notifications/devices

Push sending is disabled when FCM credentials are absent; sends are then
recorded as skipped rather than reported as sent.

Bulk CSV import (SwiftChainn#132)

Adds POST /api/v1/deliveries/bulk accepting multipart/form-data. Includes
an in-repo RFC 4180 parser supporting quoted fields, escaped quotes,
embedded newlines and CRLF, tracking each row's source line number for
error reporting.

Partial success is the expected outcome: valid rows are inserted with an
unordered insertMany and every rejected row is reported with its original
line number and reason. Duplicate tracking numbers are detected both
within the file and against existing records, the latter in a single
query. Responds 201 when every row imported, 207 on partial success and
422 when the file parsed but no row could be imported.

Socket handler decoupling (SwiftChainn#117)

Extracts chat business logic out of the Socket.IO event listeners into
ChatMessageService, which takes and returns plain values and touches no
socket, namespace or event name. socketService.ts becomes a thin
transport adapter and socketController.ts only registers listeners.

Socket payloads bypass Express middleware, so the extracted service is
now the validation boundary for realtime input; invalid messages are
rejected with a reason sent to the originating socket instead of failing
silently.

Testing

Adds 148 tests across six suites, run against a real in-process MongoDB
via mongodb-memory-server rather than mocked Mongoose. Coverage includes
the concurrency guarantees of the conditional status updates, CSV edge
cases, partial-failure imports, notification opt-out and token-pruning
paths, and the delivery state machine.

Introduces no new type errors: the repository reports 75 pre-existing
tsc errors both before and after this change.
Bulk import reported failureCount as the length of the error list, but a
single row raises one error per invalid column. A one-row file with three
bad fields reported "3 of 1 rows failed", and the controller's summary
message could claim more failures than the file had rows. failureCount is
now the number of distinct rejected lines. Covered by two regression
tests asserting successCount + failureCount never exceeds totalRows.

Adds 27 tests for FcmProvider, the one component that cannot be exercised
against the live service. axios is mocked at the HTTP boundary; everything
on this side of it is real. The service-account JWT is signed with a
generated RSA key pair and verified in-test, covering the RS256 header,
the firebase.messaging scope, the one-hour expiry and the escaped-newline
normalisation that .env-supplied PEM keys need.

Also covers the failure classification that decides whether a device
registration survives: UNREGISTERED, INVALID_ARGUMENT and
SENDER_ID_MISMATCH prune the token, while transient failures such as
INTERNAL, UNAVAILABLE and QUOTA_EXCEEDED leave it registered. Access
token caching and single-flight fetching under concurrent sends are
asserted, as is the guarantee that an authentication failure reports
cleanly without pruning any token.

Applies prettier formatting to the files added in the previous commit and
annotates the notification preference default as a factory so each
document gets its own array.

Suite total for this branch: 177 tests across 7 files, all passing.
@drips-wave

drips-wave Bot commented Aug 30, 2026

Copy link
Copy Markdown

@Adeolu01 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

@Tybravo

Tybravo commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

@Adeolu01
Thank you for contributing

@Tybravo
Tybravo merged commit 918a6af into SwiftChainn:main Aug 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants