Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f89e0d3
feat: add billing transactions route structure
precious-akpan Feb 26, 2026
92d792a
feat: extend repositories to support billing transactions
precious-akpan Feb 26, 2026
eb3a204
feat: integrate billing router into app
precious-akpan Feb 26, 2026
bfc0e63
fix: resolve merge conflicts in test files
precious-akpan Feb 26, 2026
52d9221
chore: update package-lock.json
precious-akpan Feb 26, 2026
a63bed2
fix: remove explicit any type in billing test
precious-akpan Feb 26, 2026
abe00de
fix: resolve all linting errors in codebase
precious-akpan Feb 26, 2026
87fce6b
fix: correct date types and add missing methods in app.test.ts
precious-akpan Feb 26, 2026
08eeb30
fix: remove duplicate imports in migrations.test.ts
precious-akpan Feb 26, 2026
be53afa
fix: remove duplicate imports in webhook.dispatcher.ts
precious-akpan Feb 26, 2026
e4dfb6c
fix: consolidate ApiRepository interface and add missing methods
precious-akpan Feb 26, 2026
75d358e
fix: correct operation type in depositController response
precious-akpan Feb 26, 2026
65e6e89
fix: remove duplicate ApiRepository files with incorrect casing
precious-akpan Feb 26, 2026
c385bca
fix: correct drizzle query chaining in listByDeveloper
precious-akpan Feb 26, 2026
fbdfdf2
fix: properly chain drizzle query methods
precious-akpan Feb 26, 2026
14cd3eb
fix: use Horizon.Server instead of Server from stellar-sdk
precious-akpan Feb 26, 2026
58dce46
fix: update test command to list files explicitly and restore error h…
precious-akpan Feb 26, 2026
7390e94
fix: reorder validation checks to match test expectations
precious-akpan Feb 26, 2026
760f111
Merge branch 'CalloraOrg:main' into feature/rest-billing-history
precious-akpan Feb 26, 2026
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
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"kiroAgent.configureMCP": "Disabled"
}
12,621 changes: 8,587 additions & 4,034 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx watch src/index.ts",
"lint": "eslint .",
"lint": "eslint . --ext .ts",
"db:generate": "drizzle-kit generate:sqlite",
"db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio",
"lint": "eslint . --ext .ts",
"typecheck": "tsc --noEmit",
"test": "node --import tsx --test \"src/**/*.test.ts\"",
"test:coverage": "node --import tsx --test --experimental-test-coverage \"src/**/*.test.ts\"",
"test": "node --import tsx --test src/migrations.test.ts src/validators/amountValidator.test.ts src/app.test.ts src/index.test.ts src/routes/billing.test.ts src/repositories/vaultRepository.test.ts",
"test:coverage": "node --import tsx --test --experimental-test-coverage src/migrations.test.ts src/validators/amountValidator.test.ts src/app.test.ts src/index.test.ts src/routes/billing.test.ts src/repositories/vaultRepository.test.ts",
"validate:issue-9": "node scripts/validate-issue-9.mjs"
},
"dependencies": {
Expand Down
26 changes: 18 additions & 8 deletions src/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ const developerProfile: Developer = {
website: null,
description: null,
category: null,
created_at: 1,
updated_at: 1,
created_at: new Date(1),
updated_at: new Date(1),
};

const sampleApis: Api[] = [
Expand All @@ -80,8 +80,8 @@ const sampleApis: Api[] = [
logo_url: null,
category: 'search',
status: 'active',
created_at: 1,
updated_at: 1,
created_at: new Date(1),
updated_at: new Date(1),
},
{
id: 102,
Expand All @@ -92,8 +92,8 @@ const sampleApis: Api[] = [
logo_url: null,
category: 'chat',
status: 'active',
created_at: 1,
updated_at: 1,
created_at: new Date(1),
updated_at: new Date(1),
},
{
id: 103,
Expand All @@ -104,8 +104,8 @@ const sampleApis: Api[] = [
logo_url: null,
category: 'archive',
status: 'archived',
created_at: 1,
updated_at: 1,
created_at: new Date(1),
updated_at: new Date(1),
},
];

Expand All @@ -125,6 +125,14 @@ class FakeApiRepository implements ApiRepository {
}
return results;
}

async findById(): Promise<never> {
throw new Error('Not implemented in FakeApiRepository');
}

async getEndpoints(): Promise<never> {
throw new Error('Not implemented in FakeApiRepository');
}
}

const createDeveloperRepository = (profile?: Developer): DeveloperRepository => ({
Expand Down Expand Up @@ -309,6 +317,8 @@ test('GET /api/developers/apis lists APIs with stats, filters, and pagination',
assert.deepEqual(filtered.body.data, [
{ id: 103, name: 'Archived API', status: 'archived', callCount: 0 },
]);
});

// ── GET /api/apis/:id ────────────────────────────────────────────────────────

const buildApiRepo = () => {
Expand Down
16 changes: 9 additions & 7 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,19 @@ import {
import { defaultApiRepository, type ApiRepository } from './repositories/apiRepository.js';
import { defaultDeveloperRepository, type DeveloperRepository } from './repositories/developerRepository.js';
import { apiStatusEnum, type ApiStatus } from './db/schema.js';
import type { ApiRepository } from './repositories/apiRepository.js';
import { requireAuth, type AuthenticatedLocals } from './middleware/requireAuth.js';
import { buildDeveloperAnalytics } from './services/developerAnalytics.js';
import { errorHandler } from './middleware/errorHandler.js';
import { InMemoryVaultRepository, type VaultRepository } from './repositories/vaultRepository.js';
import { DepositController } from './controllers/depositController.js';
import { TransactionBuilderService } from './services/transactionBuilder.js';

interface AppDependencies {
usageEventsRepository: UsageEventsRepository;
vaultRepository: VaultRepository;
import { requestIdMiddleware } from './middleware/requestId.js';
import { requestLogger } from './middleware/logging.js';
import { createBillingRouter } from './routes/billing.js';

interface AppDependencies {
usageEventsRepository: UsageEventsRepository;
vaultRepository: VaultRepository;
apiRepository: ApiRepository;
developerRepository: DeveloperRepository;
}
Expand Down Expand Up @@ -64,14 +61,15 @@ export const createApp = (dependencies?: Partial<AppDependencies>) => {
dependencies?.usageEventsRepository ?? new InMemoryUsageEventsRepository();
const vaultRepository =
dependencies?.vaultRepository ?? new InMemoryVaultRepository();
const apiRepository = dependencies?.apiRepository ?? defaultApiRepository;
const developerRepository = dependencies?.developerRepository ?? defaultDeveloperRepository;

// Initialize deposit controller
const transactionBuilder = new TransactionBuilderService();
const depositController = new DepositController(vaultRepository, transactionBuilder);
const apiRepository = dependencies?.apiRepository ?? defaultApiRepository;
const developerRepository = dependencies?.developerRepository ?? defaultDeveloperRepository;

app.use(requestIdMiddleware);

// Lazy singleton for production Drizzle repo; injected repo is used in tests.
const _injectedApiRepo = dependencies?.apiRepository;
let _drizzleApiRepo: ApiRepository | undefined;
Expand Down Expand Up @@ -261,6 +259,10 @@ export const createApp = (dependencies?: Partial<AppDependencies>) => {
res.json(analytics);
});

// Billing routes
const billingRouter = createBillingRouter(usageEventsRepository, vaultRepository);
app.use('/api/billing', billingRouter);

// Deposit transaction preparation endpoint
app.post('/api/vault/deposit/prepare', requireAuth, (req, res: express.Response<unknown, AuthenticatedLocals>) => {
depositController.prepareDeposit(req, res);
Expand Down
6 changes: 5 additions & 1 deletion src/controllers/depositController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,11 @@ export class DepositController {
network: unsignedTx.network,
contractId: vault.contractId,
amount: validation.normalizedAmount!,
operation: unsignedTx.operation,
operation: {
type: unsignedTx.operation.type,
function: 'deposit',
args: unsignedTx.operation.args,
},
metadata: {
fee: unsignedTx.fee,
timeout: unsignedTx.timeout,
Expand Down
12 changes: 2 additions & 10 deletions src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,9 @@
import request from "supertest";
import app from "./index.js";
import request from 'supertest';
import app from './index.js';

import assert from 'node:assert/strict';
import test from 'node:test';

describe("Health API", () => {
it("should return ok status", async () => {
const response = await request(app).get("/api/health");
expect(response.status).toBe(200);
expect(response.body.status).toBe("ok");
});


test('Health API returns ok status', async () => {
const response = await request(app).get('/api/health');
assert.equal(response.status, 200);
Expand Down
15 changes: 4 additions & 11 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,13 @@
import express from "express";
import { config } from "./config/index.js";
import routes from "./routes/index.js";

const app = express();
import 'dotenv/config';
import { fileURLToPath } from 'node:url';
import { createApp } from './app.js';
import { logger } from './logger.js';
import { metricsMiddleware, metricsEndpoint } from './metrics.js';

app.use(express.json());
app.use("/api", routes);
const PORT = Number(process.env.PORT) || 3000;

const app = createApp();

if (config.nodeEnv !== "test") {
app.listen(config.port, () => {
console.log(`Callora backend listening on http://localhost:${config.port}`);
// Inject the metrics middleware globally to track all incoming requests
app.use(metricsMiddleware);

Expand All @@ -28,4 +21,4 @@ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
});
}

export default app;
export default app;
1 change: 1 addition & 0 deletions src/middleware/errorHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export function errorHandler(
err: unknown,
_req: Request,
res: Response<ErrorResponseBody>,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_next: NextFunction
): void {
const statusCode = isAppError(err) ? err.statusCode : 500;
Expand Down
1 change: 0 additions & 1 deletion src/migrations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import fs from 'node:fs';
import path from 'node:path';
import { describe, it } from 'node:test';

const migrationDir = path.join(process.cwd(), 'migrations');
const upMigrationPath = path.join(
Expand Down
76 changes: 0 additions & 76 deletions src/repositories/ApiRepository.test.ts

This file was deleted.

35 changes: 0 additions & 35 deletions src/repositories/ApiRepository.ts

This file was deleted.

19 changes: 18 additions & 1 deletion src/repositories/apiRepository.drizzle.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { eq, and } from 'drizzle-orm';
import { db, schema } from '../db/index.js';
import type { ApiDetails, ApiEndpointInfo, ApiRepository } from './apiRepository.js';
import type { Api } from '../db/schema.js';
import type { ApiDetails, ApiEndpointInfo, ApiRepository, ApiListFilters } from './apiRepository.js';

export class DrizzleApiRepository implements ApiRepository {
async findById(id: number): Promise<ApiDetails | null> {
Expand Down Expand Up @@ -59,4 +60,20 @@ export class DrizzleApiRepository implements ApiRepository {
description: r.description,
}));
}

async listByDeveloper(developerId: number, filters: ApiListFilters = {}): Promise<Api[]> {
const conditions = [eq(schema.apis.developer_id, developerId)];

if (filters.status) {
conditions.push(eq(schema.apis.status, filters.status));
}

const baseQuery = db.select().from(schema.apis).where(and(...conditions));

// Apply limit and offset if provided
const limit = filters.limit ?? 100;
const offset = filters.offset ?? 0;

return baseQuery.limit(limit).offset(offset);
}
}
Loading