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
10 changes: 4 additions & 6 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,8 @@ SwiftChain_Backend/
│ │ └── fcmProvider.ts # Firebase Cloud Messaging implementation
│ │
│ ├── controllers/ # HTTP request handlers
│ │ ├── delivery.controller.ts # Full delivery CRUD (uses delivery.service.ts)
│ │ ├── deliveryController.ts # ⚠ DEPRECATED — ETA endpoint only
│ │ ├── deliveryCrudController.ts # ⚠ DEPRECATED — basic CRUD exports
│ │ ├── delivery.controller.ts # Canonical delivery controller (CRUD + ETA, uses delivery.service.ts)
│ │ ├── deliveryController.ts # ⚠ DEPRECATED — compatibility shim re-exporting delivery.controller.ts
│ │ ├── deliveryStatusController.ts # Status transitions
│ │ ├── escrow.controller.ts # Escrow by delivery/contract + fund trigger
│ │ ├── escrowController.ts # ⚠ DEPRECATED — admin flagged escrows
Expand Down Expand Up @@ -865,9 +864,8 @@ New routes should use the middleware in `src/middlewares/`.

| Status | File | Usage |
|---|---|---|
| Active | `src/controllers/delivery.controller.ts` | Full delivery CRUD via `delivery.service.ts` |
| **DEPRECATED** | `src/controllers/deliveryController.ts` | ETA endpoint only (`GET /:id/eta`), wires to legacy `deliveryService` |
| **DEPRECATED** | `src/controllers/deliveryCrudController.ts` | Exports `createDelivery`, `getDeliveries`, `getDeliveryById`, `assignDriver` as standalone functions |
| Active | `src/controllers/delivery.controller.ts` | Full delivery CRUD (incl. `GET /:id/eta`) via `delivery.service.ts` |
| **DEPRECATED** | `src/controllers/deliveryController.ts` | Compatibility shim — re-exports the canonical controller from `delivery.controller.ts` |

#### Routes

Expand Down
58 changes: 58 additions & 0 deletions docs/query-contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Shared Query Contract

All list endpoints expose a single, consistent query interface backed by
`buildQueryOptions` (`src/middlewares/queryMiddleware.ts`). This document is
the contract clients can rely on.

## Parameters

| Parameter | Type | Default | Description |
| --------- | ---- | ------- | ----------- |
| `page` | positive integer | `1` | 1-based page number. |
| `limit` | positive integer | route default (10/20) | Page size, clamped to the route's `maxLimit` (100 by default). |
| `sort` | comma-separated string | route default (`-createdAt`) | e.g. `sort=-createdAt,name`. Leading `-` = descending. Fields outside the route's whitelist are rejected with 400. |
| `search` | string | — | Free-text match against the route's `searchableFields` (case-insensitive, literal substring). |
| `<field>` | route-declared type | — | Direct equality filter, e.g. `?status=pending`. Only whitelisted fields are accepted. |
| `<field>[op]` | — | — | Comparison filter where `op` ∈ `eq, ne, gt, gte, lt, lte, in, nin`, e.g. `?createdAt[gte]=2026-01-01` or `?status[in]=pending,assigned`. Non-whitelisted operators/fields are rejected with 400. |

## Pagination metadata

Every paginated list response returns the same meta shape (inside the
response's `data`, alongside the collection):

```json
{
"totalItems": 42,
"totalPages": 5,
"currentPage": 1,
"limit": 10,
"hasNextPage": true,
"hasPreviousPage": false,
"nextPage": 2,
"previousPage": null
}
```

Built by `buildPaginationMeta(totalItems, page, limit)`.

## Notes

- Invalid `page`/`limit`/`sort`/filter values produce a 400 with a descriptive
message — they are never silently coerced or ignored.
- Unknown query-string fields are ignored, so adding new whitelisted fields
later is backward compatible.
- Errors surface through the global error handler in the standard envelope
(`success: false`, `error` populated).

## Endpoint configuration

| Endpoint | Filterable fields | Searchable | Sortable |
| -------- | ----------------- | ---------- | -------- |
| `GET /v1/deliveries` | `status`, `driver` | `trackingNumber`, `customer.name`, `customer.phone` | `createdAt` |
| `GET /v1/deliveries/archived` | — | — | `createdAt` |
| `GET /v1/users/deleted` | `role`, `status` | `email`, `firstName`, `lastName` | `deletedAt` |
| `GET /v1/disputes` | `status`, `reason`, `raisedBy`, `deliveryId` | — | `createdAt` |
| `GET /v1/fleets` | `isActive` | `name` | `createdAt` |
| `GET /v1/notifications` | `status`, `event` | — | `createdAt` |
| `GET /v1/webhooks` | `isActive` | — | `createdAt` |
| `GET /v1/eventlog/unprocessed` | — | — | `createdAt` |
8 changes: 5 additions & 3 deletions src/blockchain/soroban.service.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { rpc as StellarRpc } from '@stellar/stellar-sdk';
import CircuitBreaker from 'opossum';
import logger from '../config/logger';
import { sorobanRpcClient, stellarConfig } from '../config/stellar';
import { stellarConfig } from '../config/stellar';
import env from '../config/env';
import { withRetry, RetryOptions } from '../utils/rpcRetry';
import { createCircuitBreaker, fireWithBreaker } from '../utils/circuitBreaker';
import type { ISorobanRpcClient } from '../services/providers/sorobanRpcClient';
import { defaultSorobanRpcClient } from '../services/providers/adapters';

/**
* Result returned by a successful connectivity check.
Expand Down Expand Up @@ -55,7 +57,7 @@ export interface DegradedLedgerResult {
* its own errors — is not double-wrapped.
*/
export class SorobanService {
private readonly client: StellarRpc.Server;
private readonly client: ISorobanRpcClient;

/**
* Circuit breaker for this instance's Soroban RPC operations.
Expand All @@ -67,7 +69,7 @@ export class SorobanService {
/** Counter so each service instance owns an isolated breaker. */
private static instanceCount = 0;

constructor(client: StellarRpc.Server = sorobanRpcClient) {
constructor(client: ISorobanRpcClient = defaultSorobanRpcClient) {
this.client = client;

this.breaker = createCircuitBreaker<[() => Promise<unknown>], unknown>(
Expand Down
76 changes: 42 additions & 34 deletions src/controllers/delivery.controller.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { Request, Response, NextFunction } from 'express';
import httpStatus from 'http-status-codes';
import { DeliveryStatus } from '../models/Delivery';
import {
deliveryService,
CreateDeliveryInput,
UpdateDeliveryInput,
DeliveryFilter,
AssignDriverInput,
} from '../services/delivery.service';
import { sendSuccess } from '../utils/responseWrapper';
// ETA lookups live in the dedicated delivery service; aliased so the two
// service modules can be imported side by side.
import { deliveryService as deliveryEtaService } from '../services/deliveryService';
import { sendSuccess, sendError } from '../utils/responseWrapper';
import { resolveQueryOptions, buildPaginationMeta } from '../middlewares/queryMiddleware';

interface AuthenticatedRequest extends Request {
user?: { id: string };
Expand Down Expand Up @@ -46,35 +48,47 @@ export class DeliveryController {
}
}

/**
* GET /api/v1/deliveries/:id/eta
*
* Calculates the delivery ETA from the stored pickup/dropoff coordinates.
*
* Responses:
* 200 — ETA calculated successfully.
* 400 — delivery ID missing.
* 404 — delivery not found.
* 500 — routing failure or delivery without complete coordinates.
*/
async getDeliveryETA(req: Request, res: Response, _next: NextFunction): Promise<void> {
const { id } = req.params;

if (!id) {
sendError(res, 'Delivery ID is required', httpStatus.BAD_REQUEST);
return;
}

try {
const result = await deliveryEtaService.calculateDeliveryETA({ deliveryId: id });
sendSuccess(res, result, 'ETA calculated successfully', httpStatus.OK);
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
const statusCode = errorMessage.includes('not found')
? httpStatus.NOT_FOUND
: httpStatus.INTERNAL_SERVER_ERROR;
sendError(res, errorMessage || 'Failed to calculate ETA', statusCode);
}
}

async list(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const statusParam = req.query.status as string | undefined;
const statusNormalized = statusParam ? statusParam.toLowerCase() : undefined;
const validatedStatus = Object.values(DeliveryStatus).includes(
statusNormalized as DeliveryStatus,
)
? (statusNormalized as DeliveryStatus)
: undefined;

const filters: DeliveryFilter = {
status: validatedStatus,
driver: req.query.driver as string | undefined,
search: req.query.search as string | undefined,
page: req.query.page ? parseInt(req.query.page as string, 10) : 1,
limit: req.query.limit ? parseInt(req.query.limit as string, 10) : 10,
};
const { filter, page, limit, sort } = resolveQueryOptions(req);

const result = await deliveryService.list(filters);
const result = await deliveryService.list({ filter, page, limit, sort });
sendSuccess(
res,
{
deliveries: result.data,
meta: {
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages,
},
meta: buildPaginationMeta(result.total, result.page, result.limit),
},
'Deliveries retrieved successfully',
httpStatus.OK,
Expand Down Expand Up @@ -123,20 +137,14 @@ export class DeliveryController {

async listArchived(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const page = req.query.page ? parseInt(req.query.page as string, 10) : 1;
const limit = req.query.limit ? parseInt(req.query.limit as string, 10) : 10;
const { page, limit, sort } = resolveQueryOptions(req);

const result = await deliveryService.listArchived(page, limit);
const result = await deliveryService.listArchived(page, limit, sort);
sendSuccess(
res,
{
deliveries: result.data,
meta: {
total: result.total,
page: result.page,
limit: result.limit,
totalPages: result.totalPages,
},
meta: buildPaginationMeta(result.total, result.page, result.limit),
},
'Archived deliveries retrieved successfully',
httpStatus.OK,
Expand Down
37 changes: 9 additions & 28 deletions src/controllers/deliveryController.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,9 @@
import { Request, Response } from 'express';
import { StatusCodes } from 'http-status-codes';
import { deliveryService } from '../services/deliveryService';
import { sendSuccess, sendError } from '../utils/responseWrapper';

class DeliveryController {
async getDeliveryETA(req: Request, res: Response): Promise<void> {
const { id } = req.params;

if (!id) {
sendError(res, 'Delivery ID is required', StatusCodes.BAD_REQUEST);
return;
}

try {
const result = await deliveryService.calculateDeliveryETA({ deliveryId: id });
sendSuccess(res, result, 'ETA calculated successfully', StatusCodes.OK);
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
const statusCode = errorMessage.includes('not found')
? StatusCodes.NOT_FOUND
: StatusCodes.INTERNAL_SERVER_ERROR;
sendError(res, errorMessage || 'Failed to calculate ETA', statusCode);
}
}
}

export const deliveryController = new DeliveryController();
/**
* @deprecated Deprecated compatibility shim — canonical implementation lives in
* `delivery.controller.ts` (single canonical delivery controller).
*
* Re-exports the canonical `DeliveryController` singleton so existing imports
* of `./deliveryController` keep working during the migration. New code must
* import from `./delivery.controller` instead.
*/
export { DeliveryController, deliveryController } from './delivery.controller';
Loading