Skip to content
Open
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
21 changes: 21 additions & 0 deletions CHANGELOG_API.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# API Changelog - Nepa Billing System

This document tracks all changes made to the Nepa API, following [Semantic Versioning](https://semver.org/).

## [1.0.0] - 2026-02-24

### Added

- **Developer Portal**: Integrated RapiDoc for a modern, interactive documentation experience at `/docs`.
- **API Aggregator**: Centralized documentation access via the API Gateway.
- **Analytics**: Integrated `swagger-stats` for real-time API usage insights at `/api-stats`.
- **User Service Documentation**: Added comprehensive OpenAPI 3.0 annotations for the User Service.
- **Enhanced Documentation**: Added code examples and standardized security schemes for all endpoints.

### Changed

- Moved documentation entry point from root `app.ts` to `microservices/api-gateway`.

---

_Generated by Antigravity_
67 changes: 67 additions & 0 deletions microservices/api-gateway/docs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Express, Request, Response } from "express";
import axios from "axios";
import { swaggerSpec } from "../../src/config/swagger";

/**
* RapiDoc HTML Template
* Provides a premium, interactive developer portal experience.
*/
const getRapiDocHtml = (specUrl: string) => `
<!doctype html>
<html>
<head>
<title>Nepa Developer Portal</title>
<meta charset="utf-8">
<script type="module" src="https://unpkg.com/rapidoc/dist/rapidoc-min.js"></script>
<style>
body { margin: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
rapi-doc { width: 100%; height: 100vh; }
</style>
</head>
<body>
<rapi-doc
spec-url="${specUrl}"
theme="dark"
render-style="read"
schema-style="table"
show-header="true"
allow-authentication="true"
allow-server-selection="true"
allow-api-list-style-selection="true"
id="portal"
>
<img slot="nav-logo" src="https://via.placeholder.com/40" style="margin-left:20px" />
<div slot="header" style="display:flex; align-items:center; margin-left:20px;">
<span style="font-size:20px; font-weight:bold; color: #fff;">NEPA Developer Portal</span>
</div>
</rapi-doc>
</body>
</html>
`;

export const setupDocs = (app: Express) => {
// 1. Serve the combined OpenAPI Spec
app.get("/api-docs/openapi.json", (req: Request, res: Response) => {
res.json(swaggerSpec);
});

// 2. Serve the RapiDoc Portal
app.get("/docs", (req: Request, res: Response) => {
res.send(getRapiDocHtml("/api-docs/openapi.json"));
});

// 3. API Statistics (Swagger Stats)
// This satisfies the "API analytics and usage insights" requirement
try {
const swStats = require("swagger-stats");
app.use(
swStats.getMiddleware({
swaggerSpec: swaggerSpec,
uriPath: "/api-stats",
name: "Nepa API Gateway",
}),
);
} catch (e) {
console.warn("swagger-stats not loaded. Run npm install.");
}
};
4 changes: 2 additions & 2 deletions microservices/api-gateway/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ app.get('/health', async (req, res) => {
const response = await axios.get(`${url}/health`, { timeout: 1500 });
return { service: name, status: response.status < 500 ? 'UP' : 'DEGRADED' };
} catch {
return { service: name, status: 'DOWN' };
return { service: name, status: "DOWN" };
}
})
}),
);

const allUp = gatewayHealth.status === 'UP' && downstream.every(s => s.status === 'UP');
Expand Down
121 changes: 85 additions & 36 deletions microservices/payment-service/server.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import { paymentClient } from '../../databases/clients';
import { createLogger } from '../shared/utils/logger';
import { requestIdMiddleware } from '../shared/middleware/requestId';
import { errorHandler } from '../shared/middleware/errorHandler';
import { sendSuccess, sendError } from '../shared/utils/response';
import { OpenTelemetrySetup } from '../../observability/tracing/OpenTelemetrySetup';
import EventBus from '../../databases/event-patterns/EventBus';
import { createPaymentSuccessEvent, createPaymentFailedEvent } from '../../databases/event-patterns/events';

const SERVICE_NAME = 'payment-service';
import express from "express";
import cors from "cors";
import helmet from "helmet";
import { paymentClient } from "../../databases/clients";
import { createLogger } from "../shared/utils/logger";
import { requestIdMiddleware } from "../shared/middleware/requestId";
import { errorHandler } from "../shared/middleware/errorHandler";
import { sendSuccess, sendError } from "../shared/utils/response";
import { OpenTelemetrySetup } from "../../observability/tracing/OpenTelemetrySetup";
import EventBus from "../../databases/event-patterns/EventBus";
import {
createPaymentSuccessEvent,
createPaymentFailedEvent,
} from "../../databases/event-patterns/events";

const SERVICE_NAME = "payment-service";
const PORT = process.env.PAYMENT_SERVICE_PORT || 3002;
const logger = createLogger(SERVICE_NAME);

Expand All @@ -24,64 +27,110 @@ app.use(cors());
app.use(express.json());
app.use(requestIdMiddleware(SERVICE_NAME));

app.get('/health', async (req, res) => {
app.get("/health", async (req, res) => {
try {
await paymentClient.$queryRaw`SELECT 1`;
sendSuccess(res, {
status: 'UP',
status: "UP",
timestamp: new Date().toISOString(),
uptime: process.uptime(),
service: SERVICE_NAME,
version: '1.0.0',
dependencies: { database: 'UP' },
version: "1.0.0",
dependencies: { database: "UP" },
});
} catch (error) {
sendError(res, 'HEALTH_CHECK_FAILED', 'Service unhealthy', 503);
sendError(res, "HEALTH_CHECK_FAILED", "Service unhealthy", 503);
}
});

app.post('/payments', async (req, res, next) => {
/**
* @openapi
* /payments:
* post:
* tags:
* - Payments
* summary: Process a new payment
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/PaymentCreate'
* responses:
* 201:
* description: Payment processed successfully
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Payment'
*/
app.post("/payments", async (req, res, next) => {
try {
const payment = await paymentClient.payment.create({
data: req.body,
});

EventBus.publish(createPaymentSuccessEvent(
payment.id,
payment.billId,
payment.userId,
payment.amount
));


EventBus.publish(
createPaymentSuccessEvent(
payment.id,
payment.billId,
payment.userId,
payment.amount,
),
);

sendSuccess(res, payment, 201);
} catch (error) {
next(error);
}
});

app.get('/payments/:id', async (req, res, next) => {
/**
* @openapi
* /payments/{id}:
* get:
* tags:
* - Payments
* summary: Get payment by ID
* parameters:
* - name: id
* in: path
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Payment found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Payment'
* 404:
* description: Payment not found
*/
app.get("/payments/:id", async (req, res, next) => {
try {
const payment = await paymentClient.payment.findUnique({
where: { id: req.params.id },
});

if (!payment) {
return sendError(res, 'PAYMENT_NOT_FOUND', 'Payment not found', 404);
return sendError(res, "PAYMENT_NOT_FOUND", "Payment not found", 404);
}

sendSuccess(res, payment);
} catch (error) {
next(error);
}
});

app.get('/payments/user/:userId', async (req, res, next) => {
app.get("/payments/user/:userId", async (req, res, next) => {
try {
const payments = await paymentClient.payment.findMany({
where: { userId: req.params.userId },
orderBy: { createdAt: 'desc' },
orderBy: { createdAt: "desc" },
});

sendSuccess(res, payments);
} catch (error) {
next(error);
Expand All @@ -94,8 +143,8 @@ app.listen(PORT, () => {
logger.info(`${SERVICE_NAME} listening on port ${PORT}`);
});

process.on('SIGTERM', async () => {
logger.info('SIGTERM received, shutting down gracefully');
process.on("SIGTERM", async () => {
logger.info("SIGTERM received, shutting down gracefully");
await paymentClient.$disconnect();
await tracing.shutdown();
process.exit(0);
Expand Down
Loading