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
5 changes: 5 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001
LOG_LEVEL=
FRONTEND_URL=http://localhost:3000

# Server Configuration
PORT=3001

DATABASE_URL=postgres://postgres:postgres@db:5432/remitlend
# Docker default (use redis://localhost:6379 if running Redis outside docker-compose)
REDIS_URL=redis://redis:6379
Expand Down Expand Up @@ -44,6 +47,8 @@ SCORE_DELTA_LATE=5
# Indexer Configuration
INDEXER_POLL_INTERVAL_MS=30000
INDEXER_BATCH_SIZE=100
# Maximum ledger lag before /health/deep reports indexer as degraded
INDEXER_HEALTH_LAG_LIMIT=100

# Default checker (on-chain `check_defaults`)
# How often to scan + submit default checks while the API process is running
Expand Down
39 changes: 17 additions & 22 deletions backend/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,43 +5,38 @@ module.exports = {
es2021: true,
jest: true,
},
parser: "@typescript-eslint/parser",
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2020,
sourceType: "module",
sourceType: 'module',
tsconfigRootDir: __dirname,
project: "./tsconfig.json",
project: './tsconfig.json',
},
plugins: ["@typescript-eslint", "prettier"],
plugins: ['@typescript-eslint', 'prettier'],
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
rules: {
"prettier/prettier": "error",
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
"@typescript-eslint/explicit-module-boundary-types": "off",
"no-console": ["warn", { allow: ["warn", "error"] }],
"@typescript-eslint/no-explicit-any": "warn",
"no-constant-condition": "off",
'prettier/prettier': 'error',
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
'@typescript-eslint/explicit-module-boundary-types': 'off',
'no-console': ['warn', { allow: ['warn', 'error'] }],
'@typescript-eslint/no-explicit-any': 'warn',
'no-constant-condition': 'off',
},
overrides: [
{
files: [
"**/*.test.ts",
"**/*.spec.ts",
"src/tests/**/*.ts",
"src/**/__tests__/**/*.ts",
],
files: ['**/*.test.ts', '**/*.spec.ts', 'src/tests/**/*.ts', 'src/**/__tests__/**/*.ts'],
rules: {
"no-useless-catch": "off",
'no-useless-catch': 'off',
},
},
{
files: ["src/utils/demo*.ts"],
files: ['src/utils/demo*.ts'],
rules: {
"no-console": "off",
'no-console': 'off',
},
},
],
Expand Down
36 changes: 18 additions & 18 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ backend/
Centralized error handling middleware that catches and formats errors.

```typescript
import { errorHandler } from "./middleware/errorHandler";
import { errorHandler } from './middleware/errorHandler';
app.use(errorHandler);
```

Expand All @@ -304,30 +304,30 @@ app.use(errorHandler);
Request validation using Zod schemas.

```typescript
import { validate } from "./middleware/validation";
import { mySchema } from "./schemas/mySchemas";
import { validate } from './middleware/validation';
import { mySchema } from './schemas/mySchemas';

router.post("/endpoint", validate(mySchema), controller);
router.post('/endpoint', validate(mySchema), controller);
```

### Rate Limiter

Protects endpoints from abuse with configurable rate limits.

```typescript
import { rateLimiter } from "./middleware/rateLimiter";
app.use("/api/", rateLimiter);
import { rateLimiter } from './middleware/rateLimiter';
app.use('/api/', rateLimiter);
```

### Async Handler

Wraps async route handlers to catch errors automatically.

```typescript
import { asyncHandler } from "./middleware/asyncHandler";
import { asyncHandler } from './middleware/asyncHandler';

router.get(
"/endpoint",
'/endpoint',
asyncHandler(async (req, res) => {
// Async code here
}),
Expand Down Expand Up @@ -355,14 +355,14 @@ npm test -- --watch
### Test Structure

```typescript
import request from "supertest";
import app from "../app";
import request from 'supertest';
import app from '../app';

describe("GET /api/health", () => {
it("should return 200 OK", async () => {
const response = await request(app).get("/api/health").expect(200);
describe('GET /api/health', () => {
it('should return 200 OK', async () => {
const response = await request(app).get('/api/health').expect(200);

expect(response.body).toHaveProperty("status", "ok");
expect(response.body).toHaveProperty('status', 'ok');
});
});
```
Expand All @@ -381,9 +381,9 @@ Aim for >80% code coverage on new code. Current coverage:
### Custom Error Class

```typescript
import { AppError } from "./errors/AppError";
import { AppError } from './errors/AppError';

throw new AppError("User not found", 404);
throw new AppError('User not found', 404);
```

### Error Response Format
Expand All @@ -404,11 +404,11 @@ Validation schemas are defined using Zod in the `schemas/` directory.
### Example Schema

```typescript
import { z } from "zod";
import { z } from 'zod';

export const getUserScoreSchema = z.object({
params: z.object({
userId: z.string().min(1, "User ID is required"),
userId: z.string().min(1, 'User ID is required'),
}),
});

Expand Down
26 changes: 13 additions & 13 deletions backend/jest.config.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
import type { Config } from "jest";
import type { Config } from 'jest';

const config: Config = {
preset: "ts-jest",
testEnvironment: "node",
testMatch: ["**/*.test.ts", "**/*.spec.ts"],
setupFilesAfterEnv: ["<rootDir>/src/tests/jest.setup.js"],
moduleFileExtensions: ["ts", "js", "json", "node"],
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/*.test.ts', '**/*.spec.ts'],
setupFilesAfterEnv: ['<rootDir>/src/tests/jest.setup.js'],
moduleFileExtensions: ['ts', 'js', 'json', 'node'],
transform: {
"^.+\\.(ts|tsx)$": [
"ts-jest",
'^.+\\.(ts|tsx)$': [
'ts-jest',
{
useESM: true,
tsconfig: {
module: "esnext",
moduleResolution: "bundler",
module: 'esnext',
moduleResolution: 'bundler',
},
},
],
},
extensionsToTreatAsEsm: [".ts"],
extensionsToTreatAsEsm: ['.ts'],
globals: {
"ts-jest": {
'ts-jest': {
useESM: true,
},
},
moduleNameMapper: {
// Correct pattern - strips .js so Jest finds the .ts source file
"^(./|../)(.*)\\.js$": "$1$2",
'^(./|../)(.*)\\.js$': '$1$2',
},
};

Expand Down
34 changes: 17 additions & 17 deletions backend/migrations/1771691269865_initial-schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,31 @@ export const shorthands = undefined;
* @returns {Promise<void> | void}
*/
export const up = (pgm) => {
pgm.createTable("scores", {
id: "id",
user_id: { type: "varchar(255)", notNull: true, unique: true },
current_score: { type: "integer", notNull: true, default: 500 },
pgm.createTable('scores', {
id: 'id',
user_id: { type: 'varchar(255)', notNull: true, unique: true },
current_score: { type: 'integer', notNull: true, default: 500 },
updated_at: {
type: "timestamp",
type: 'timestamp',
notNull: true,
default: pgm.func("current_timestamp"),
default: pgm.func('current_timestamp'),
},
});

pgm.createTable("remittance_history", {
id: "id",
user_id: { type: "varchar(255)", notNull: true },
amount: { type: "numeric", notNull: true },
month: { type: "varchar(50)", notNull: true },
status: { type: "varchar(50)", notNull: true },
pgm.createTable('remittance_history', {
id: 'id',
user_id: { type: 'varchar(255)', notNull: true },
amount: { type: 'numeric', notNull: true },
month: { type: 'varchar(50)', notNull: true },
status: { type: 'varchar(50)', notNull: true },
created_at: {
type: "timestamp",
type: 'timestamp',
notNull: true,
default: pgm.func("current_timestamp"),
default: pgm.func('current_timestamp'),
},
});

pgm.createIndex("remittance_history", "user_id");
pgm.createIndex('remittance_history', 'user_id');
};

/**
Expand All @@ -42,6 +42,6 @@ export const up = (pgm) => {
* @returns {Promise<void> | void}
*/
export const down = (pgm) => {
pgm.dropTable("remittance_history");
pgm.dropTable("scores");
pgm.dropTable('remittance_history');
pgm.dropTable('scores');
};
56 changes: 28 additions & 28 deletions backend/migrations/1771691269866_loan-events-schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,41 +9,41 @@ export const shorthands = undefined;
* @returns {Promise<void> | void}
*/
export const up = (pgm) => {
pgm.createTable("loan_events", {
id: "id",
event_id: { type: "varchar(255)", notNull: true, unique: true },
event_type: { type: "varchar(50)", notNull: true },
loan_id: { type: "integer" },
borrower: { type: "varchar(255)", notNull: true },
amount: { type: "numeric" },
ledger: { type: "integer", notNull: true },
ledger_closed_at: { type: "timestamp", notNull: true },
tx_hash: { type: "varchar(255)", notNull: true },
contract_id: { type: "varchar(255)", notNull: true },
topics: { type: "jsonb" },
value: { type: "text" },
pgm.createTable('loan_events', {
id: 'id',
event_id: { type: 'varchar(255)', notNull: true, unique: true },
event_type: { type: 'varchar(50)', notNull: true },
loan_id: { type: 'integer' },
borrower: { type: 'varchar(255)', notNull: true },
amount: { type: 'numeric' },
ledger: { type: 'integer', notNull: true },
ledger_closed_at: { type: 'timestamp', notNull: true },
tx_hash: { type: 'varchar(255)', notNull: true },
contract_id: { type: 'varchar(255)', notNull: true },
topics: { type: 'jsonb' },
value: { type: 'text' },
created_at: {
type: "timestamp",
type: 'timestamp',
notNull: true,
default: pgm.func("current_timestamp"),
default: pgm.func('current_timestamp'),
},
});

pgm.createIndex("loan_events", "event_type");
pgm.createIndex("loan_events", "borrower");
pgm.createIndex("loan_events", "loan_id");
pgm.createIndex("loan_events", "ledger");
pgm.createIndex("loan_events", "tx_hash");
pgm.createIndex('loan_events', 'event_type');
pgm.createIndex('loan_events', 'borrower');
pgm.createIndex('loan_events', 'loan_id');
pgm.createIndex('loan_events', 'ledger');
pgm.createIndex('loan_events', 'tx_hash');

// Table to track indexer state
pgm.createTable("indexer_state", {
id: "id",
last_indexed_ledger: { type: "integer", notNull: true, default: 0 },
last_indexed_cursor: { type: "varchar(255)" },
pgm.createTable('indexer_state', {
id: 'id',
last_indexed_ledger: { type: 'integer', notNull: true, default: 0 },
last_indexed_cursor: { type: 'varchar(255)' },
updated_at: {
type: "timestamp",
type: 'timestamp',
notNull: true,
default: pgm.func("current_timestamp"),
default: pgm.func('current_timestamp'),
},
});

Expand All @@ -60,6 +60,6 @@ export const up = (pgm) => {
* @returns {Promise<void> | void}
*/
export const down = (pgm) => {
pgm.dropTable("indexer_state");
pgm.dropTable("loan_events");
pgm.dropTable('indexer_state');
pgm.dropTable('loan_events');
};
Loading
Loading