Skip to content

Commit 68257fb

Browse files
authored
Merge pull request #56 from GideonBature/webhook-and-delivery-apis
Implement APIs to log webhook
2 parents 570a7b6 + 6d26cf7 commit 68257fb

File tree

18 files changed

+2311
-25
lines changed

18 files changed

+2311
-25
lines changed

fluxapay_backend/.env.example

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Database
2+
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/fluxapay?schema=public"
3+
4+
# Docker Compose DB settings (optional, defaults shown)
5+
POSTGRES_USER=postgres
6+
POSTGRES_PASSWORD=postgres
7+
POSTGRES_DB=fluxapay
8+
9+
# JWT
10+
JWT_SECRET="your-jwt-secret-here"
11+
12+
# Email (Resend)
13+
RESEND_API_KEY="re_your_resend_api_key_here"
14+
15+
# Webhook
16+
WEBHOOK_SECRET="your-webhook-secret-here"

fluxapay_backend/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ dist
33
.env
44
.DS_Store
55
coverage
6+
/docker-compose.yml
7+
/docker-compose.yaml
68

79
/src/generated/
810
/prisma/migrations/

fluxapay_backend/prisma/schema.prisma

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ model Merchant {
2727
created_at DateTime @default(now())
2828
updated_at DateTime @updatedAt
2929
otps OTP[]
30+
webhookLogs WebhookLog[]
3031
}
3132

3233
model OTP {
@@ -49,4 +50,59 @@ enum MerchantStatus {
4950
enum OTPChannel {
5051
email
5152
phone
53+
}
54+
55+
model WebhookLog {
56+
id String @id @default(cuid())
57+
merchant Merchant @relation(fields: [merchantId], references: [id])
58+
merchantId String
59+
event_type WebhookEventType
60+
endpoint_url String
61+
request_payload Json
62+
response_body String?
63+
http_status Int?
64+
status WebhookStatus @default(pending)
65+
payment_id String?
66+
retry_count Int @default(0)
67+
max_retries Int @default(3)
68+
next_retry_at DateTime?
69+
created_at DateTime @default(now())
70+
updated_at DateTime @updatedAt
71+
retryAttempts WebhookRetryAttempt[]
72+
73+
@@index([merchantId])
74+
@@index([event_type])
75+
@@index([status])
76+
@@index([payment_id])
77+
}
78+
79+
model WebhookRetryAttempt {
80+
id String @id @default(cuid())
81+
webhookLog WebhookLog @relation(fields: [webhookLogId], references: [id])
82+
webhookLogId String
83+
attempt_number Int
84+
http_status Int?
85+
response_body String?
86+
error_message String?
87+
created_at DateTime @default(now())
88+
89+
@@index([webhookLogId])
90+
}
91+
92+
enum WebhookEventType {
93+
payment_completed
94+
payment_failed
95+
payment_pending
96+
refund_completed
97+
refund_failed
98+
subscription_created
99+
subscription_cancelled
100+
subscription_renewed
101+
}
102+
103+
enum WebhookStatus {
104+
pending
105+
delivered
106+
failed
107+
retrying
52108
}

fluxapay_backend/src/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import swaggerUi from "swagger-ui-express";
44
import { specs } from "./docs/swagger";
55
import { PrismaClient } from "./generated/client/client";
66
import merchantRoutes from "./routes/merchant.route";
7+
import webhookRoutes from "./routes/webhook.route";
78

89
const app = express();
910
const prisma = new PrismaClient();
@@ -15,6 +16,7 @@ app.use(express.json());
1516
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(specs));
1617

1718
app.use("/api/merchants", merchantRoutes);
19+
app.use("/api/webhooks", webhookRoutes);
1820
// Basic health check
1921
app.get("/health", (req, res) => {
2022
res.json({ status: "ok", timestamp: new Date() });
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { Request, Response } from "express";
2+
import z from "zod";
3+
import * as webhookSchema from "../schemas/webhook.schema";
4+
import {
5+
getWebhookLogsService,
6+
getWebhookLogDetailsService,
7+
retryWebhookService,
8+
sendTestWebhookService,
9+
} from "../services/webhook.service";
10+
import { AuthRequest } from "../types/express";
11+
import { validateUserId } from "../helpers/request.helper";
12+
13+
type GetWebhookLogsQuery = z.infer<typeof webhookSchema.getWebhookLogsSchema>;
14+
type SendTestWebhookBody = z.infer<typeof webhookSchema.sendTestWebhookSchema>;
15+
16+
export async function getWebhookLogs(req: AuthRequest, res: Response) {
17+
try {
18+
const merchantId = await validateUserId(req);
19+
const query = req.query as unknown as GetWebhookLogsQuery;
20+
21+
const result = await getWebhookLogsService({
22+
merchantId,
23+
event_type: query.event_type as any,
24+
status: query.status as any,
25+
date_from: query.date_from,
26+
date_to: query.date_to,
27+
search: query.search,
28+
page: Number(query.page) || 1,
29+
limit: Number(query.limit) || 10,
30+
});
31+
32+
res.status(200).json(result);
33+
} catch (err: any) {
34+
console.error(err);
35+
res.status(err.status || 500).json({ message: err.message || "Server error" });
36+
}
37+
}
38+
39+
export async function getWebhookLogDetails(req: AuthRequest, res: Response) {
40+
try {
41+
const merchantId = await validateUserId(req);
42+
const { log_id } = req.params;
43+
44+
if (!log_id || Array.isArray(log_id)) {
45+
return res.status(400).json({ message: "Log ID is required" });
46+
}
47+
48+
const result = await getWebhookLogDetailsService({
49+
merchantId,
50+
log_id,
51+
});
52+
53+
res.status(200).json(result);
54+
} catch (err: any) {
55+
console.error(err);
56+
res.status(err.status || 500).json({ message: err.message || "Server error" });
57+
}
58+
}
59+
60+
export async function retryWebhook(req: AuthRequest, res: Response) {
61+
try {
62+
const merchantId = await validateUserId(req);
63+
const { log_id } = req.params;
64+
65+
if (!log_id || Array.isArray(log_id)) {
66+
return res.status(400).json({ message: "Log ID is required" });
67+
}
68+
69+
const result = await retryWebhookService({
70+
merchantId,
71+
log_id,
72+
});
73+
74+
res.status(200).json(result);
75+
} catch (err: any) {
76+
console.error(err);
77+
res.status(err.status || 500).json({ message: err.message || "Server error" });
78+
}
79+
}
80+
81+
export async function sendTestWebhook(req: AuthRequest, res: Response) {
82+
try {
83+
const merchantId = await validateUserId(req);
84+
const body = req.body as SendTestWebhookBody;
85+
86+
const result = await sendTestWebhookService({
87+
merchantId,
88+
event_type: body.event_type as any,
89+
endpoint_url: body.endpoint_url,
90+
payload_override: body.payload_override,
91+
});
92+
93+
res.status(200).json(result);
94+
} catch (err: any) {
95+
console.error(err);
96+
res.status(err.status || 500).json({ message: err.message || "Server error" });
97+
}
98+
}

fluxapay_backend/src/generated/client/browser.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,23 @@ import * as Prisma from './internal/prismaNamespaceBrowser'
1717
export { Prisma }
1818
export * as $Enums from './enums'
1919
export * from './enums';
20-
20+
/**
21+
* Model Merchant
22+
*
23+
*/
24+
export type Merchant = Prisma.MerchantModel
25+
/**
26+
* Model OTP
27+
*
28+
*/
29+
export type OTP = Prisma.OTPModel
30+
/**
31+
* Model WebhookLog
32+
*
33+
*/
34+
export type WebhookLog = Prisma.WebhookLogModel
35+
/**
36+
* Model WebhookRetryAttempt
37+
*
38+
*/
39+
export type WebhookRetryAttempt = Prisma.WebhookRetryAttemptModel

fluxapay_backend/src/generated/client/client.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ export * from "./enums"
2727
* @example
2828
* ```
2929
* const prisma = new PrismaClient()
30-
* // Fetch zero or more Users
31-
* const users = await prisma.user.findMany()
30+
* // Fetch zero or more Merchants
31+
* const merchants = await prisma.merchant.findMany()
3232
* ```
3333
*
3434
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client).
@@ -39,7 +39,26 @@ export { Prisma }
3939

4040

4141
// file annotations for bundling tools to include these files
42-
path.join(__dirname, "libquery_engine-debian-openssl-3.0.x.so.node")
43-
path.join(process.cwd(), "src/generated/client/libquery_engine-debian-openssl-3.0.x.so.node")
44-
42+
path.join(__dirname, "libquery_engine-darwin-arm64.dylib.node")
43+
path.join(process.cwd(), "src/generated/client/libquery_engine-darwin-arm64.dylib.node")
4544

45+
/**
46+
* Model Merchant
47+
*
48+
*/
49+
export type Merchant = Prisma.MerchantModel
50+
/**
51+
* Model OTP
52+
*
53+
*/
54+
export type OTP = Prisma.OTPModel
55+
/**
56+
* Model WebhookLog
57+
*
58+
*/
59+
export type WebhookLog = Prisma.WebhookLogModel
60+
/**
61+
* Model WebhookRetryAttempt
62+
*
63+
*/
64+
export type WebhookRetryAttempt = Prisma.WebhookRetryAttemptModel

0 commit comments

Comments
 (0)