Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ curl http://localhost:3000/api/v1/disputes/<DISPUTE_ID>
## Checklist

- [ ] Code follows repo conventions and lints
- [ ] New files follow [the code conventions](../docs/code-conventions.md) and do not introduce duplicate legacy stacks
- [ ] Unit/integration tests added for critical logic (could be added in follow-up)
- [x] All existing tests pass
- [x] PR references related issue: Closes #129
Expand Down
60 changes: 60 additions & 0 deletions docs/ci-pipeline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# CI/CD Pipeline

Use this checklist before opening a pull request:

- [ ] Node.js 22.x and MongoDB 6.0 are available locally.
- [ ] Dependencies are installed with `pnpm install` and any `pnpm-lock.yaml` change is committed.
- [ ] `pnpm run lint` passes.
- [ ] `pnpm exec tsc --noEmit` passes (CI also runs `pnpm run build`).
- [ ] `pnpm test:coverage` passes with the required coverage thresholds.

## What CI runs

The workflow in `.github/workflows/ci.yml` runs on pushes and pull requests targeting `main` or
`develop`. Its environment matrix uses Node.js 22.x and MongoDB 6.0. The job installs pnpm 9,
starts MongoDB, installs dependencies, then runs these stages:

```bash
pnpm install
pnpm run lint
pnpm run build
pnpm test:coverage
```

`pnpm run build` invokes `tsc` and is the CI compilation gate. The equivalent no-emit check is
useful during development because it validates TypeScript without writing `dist/`:

```bash
pnpm exec tsc --noEmit
```

Tests use MongoDB at `mongodb://localhost:27017/swiftchain_test` in CI. For a local run, start a
MongoDB 6.0-compatible server and provide a JWT secret accepted by the environment schema:

```bash
CI=true MONGO_URI=mongodb://localhost:27017/swiftchain_test \
JWT_SECRET=test-secret-key-16chars pnpm test:coverage
```

## Coverage and pull-request comments

Jest writes text, JSON summary, and `coverage/lcov.info` output to `coverage/`. The configured
minimums are:

| Scope | Branches | Functions | Lines | Statements |
| --------------- | -------: | --------: | ----: | ---------: |
| Global | 60% | 60% | 60% | 60% |
| `src/services/` | 80% | 80% | 80% | 80% |
| `src/models/` | 70% | 70% | 70% | 70% |
| `src/routes/` | 60% | 60% | 60% | 60% |

CI uploads the complete `coverage/` directory as an artifact. On pull requests, the
`romeovs/lcov-reporter-action` reads `coverage/lcov.info` and posts a coverage comment. The
comment step is allowed to continue on error, but Jest fails the job when a configured threshold
is not met.

## Lockfile and package-manager requirement

CI uses pnpm 9 and the repository's `pnpm-lock.yaml`. Run project commands with pnpm rather than
silently generating an npm or Yarn lockfile. If dependency resolution changes, review and commit
the resulting `pnpm-lock.yaml` in the same pull request.
51 changes: 51 additions & 0 deletions docs/code-conventions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Backend Code Conventions

This document prevents parallel implementations from drifting apart. New code must follow these
rules; do not create a second file solely because an older naming variant already exists.

## Layering

Keep request flow in the following direction:

```text
route -> controller -> service -> repository -> Mongoose model
```

Controllers translate HTTP input and output. Services own business rules. Repositories own
database access. Models define persistence contracts. Background jobs may call services, but must
not bypass the service/repository boundaries for new behavior.

## File names

- Services use `*.service.ts` (`delivery.service.ts`, `escrow.service.ts`).
- Controllers use `*.controller.ts` (`delivery.controller.ts`, `escrow.controller.ts`).
- Repositories use `*Repository.ts` for the existing repository convention.
- Models use PascalCase names (`Delivery.ts`, `Escrow.ts`).
- Middleware files use a descriptive camelCase name under `src/middleware/` or
`src/middlewares/`, matching the directory convention already used by the feature.
- Socket transport entrypoints are handlers under `src/sockets/`; the live server wiring belongs
in `connectionHandler.ts`.

## Exports and dependency injection

Prefer named exports for services, controllers, repositories, and model types. A default export is
acceptable only where the surrounding module already establishes a singleton default convention.
Use one camelCase Awilix token per dependency (`deliveryService`, `escrowService`,
`deliveryController`). Never register snake_case aliases or two tokens for the same implementation.
The container must point canonical tokens at canonical modules.

## Legacy files

The following files are retained only for compatibility and must not receive new features:

| Legacy file | Canonical replacement | Status |
| --------------------------------------- | ---------------------------------------- | ------------------------------------------- |
| `src/services/deliveryService.ts` | `src/services/delivery.service.ts` | Retained compatibility service (deprecated) |
| `src/controllers/deliveryController.ts` | `src/controllers/delivery.controller.ts` | Compatibility shim re-export (deprecated) |
| `src/models/deliveryModel.ts` | `src/models/Delivery.ts` | Retained legacy model (deprecated) |

Each retained legacy file has a deprecation banner. The former `src/services/escrowService.ts`,
`src/controllers/deliveryCrudController.ts`, and `src/sockets/index.ts` have been removed; use their
canonical replacements and do not recreate removed parallel stacks.

Before adding a file, search `src/` for the canonical implementation and update it when one exists.
2 changes: 1 addition & 1 deletion src/di/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import IdempotencyRecord from '../models/IdempotencyRecord';

// ─── Services ──────────────────────────────────────────────────────────────────
import authService from '../services/authService';
import { deliveryService } from '../services/deliveryService';
import { deliveryService } from '../services/delivery.service';
import { driverService } from '../services/driverService';
import { driverRatingService } from '../services/driverRatingService';
import * as fleetService from '../services/fleetService';
Expand Down
5 changes: 5 additions & 0 deletions src/services/deliveryService.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/**
* @deprecated Use `src/services/delivery.service.ts` for the canonical delivery
* service. This module remains only for the legacy ETA/QR compatibility route.
*/

import { Delivery, DeliveryStatus } from '../models/Delivery';
import { ETARequest } from './providers/routingProvider';
import type { IRoutingProvider } from './providers/routingProvider';
Expand Down
2 changes: 1 addition & 1 deletion src/services/indexerService.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import EventLog from '../models/EventLog';
import Delivery from '../models/Delivery';
import logger from '../config/logger';
import { emitDeliveryStatusUpdated } from '../sockets';
import { emitDeliveryStatusUpdated } from '../sockets/connectionHandler';
import type { ISorobanRpcClient } from './providers/sorobanRpcClient';
import { defaultSorobanRpcClient } from './providers/adapters';
export interface IndexerStatusData {
Expand Down
22 changes: 22 additions & 0 deletions tests/adminRoutes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import express from 'express';
import request from 'supertest';
import adminRoutes from '../src/routes/adminRoutes';
import errorHandler from '../src/middleware/errorHandler';

describe('admin DLQ routes', () => {
const app = express();
app.use('/v1/admin', adminRoutes);
app.use(errorHandler);

it('mounts the DLQ list route and rejects unauthenticated requests', async () => {
const response = await request(app).get('/v1/admin/dlq');

expect(response.status).toBe(401);
});

it('mounts the DLQ retry route and rejects unauthenticated requests', async () => {
const response = await request(app).post('/v1/admin/dlq/example-id/retry');

expect(response.status).toBe(401);
});
});
10 changes: 10 additions & 0 deletions tests/indexerService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { IndexerService } from '../src/services/indexerService';

describe('IndexerService module wiring', () => {
it('loads without relying on the removed webSocketService module', () => {
const service = new IndexerService();

expect(service).toBeInstanceOf(IndexerService);
expect(typeof service.processDeliveryStatusUpdated).toBe('function');
});
});