Skip to content

Commit 4203ffa

Browse files
feat: add main features
1 parent 65ca844 commit 4203ffa

10 files changed

Lines changed: 377 additions & 19 deletions

File tree

README.md

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,36 @@
1-
```
2-
npm install
3-
npm run dev
4-
```
1+
# sample-subscription-backend
2+
A simple backend to serve as a reference when building subscription or recurring bill payment apps [with Paystack](https://paystack.com/). You can [check out the documentation](https://paystack.com/docs/payments/subscriptions/) to learn more.
53

6-
```
7-
open http://localhost:3000
8-
```
4+
## Tech stack
5+
- [Hono](https://hono.dev/)
6+
- SQLite
7+
- [Drizzle ORM](https://orm.drizzle.team/)
8+
- Paystack Subscription
9+
10+
## Set up
11+
- Clone the repo:
12+
```sh
13+
git clone git@github.com:PaystackOSS/sample-subscription-backend.git
14+
```
15+
- Install dependencies
16+
```
17+
pnpm install
18+
```
19+
- Set up the DB
20+
```sh
21+
pnpm db:apply
22+
```
23+
- Start server
24+
```sh
25+
pnpm dev
26+
```
27+
- Test endpoints
28+
```sh
29+
open http://localhost:3030
30+
```
31+
32+
## Code structure
33+
The code has a simple structure where all configuration files are in the top level and core logic resides on the `src` directory:
34+
- `db`: Contains the DB schema
35+
- `routes`: Each feature is categorised by routes that is then exported for usage in the main Hono class
36+
- `schema`: Contains basic [Zod schema](https://zod.dev/) request validation

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "sample-subscription-backend",
33
"type": "module",
44
"scripts": {
5-
"db:apply": "npx drizzle-kit push",
5+
"db:apply": "NODE_OPTIONS='--import tsx' drizzle-kit push",
66
"dev": "tsx watch src/index.ts",
77
"build": "tsc",
88
"start": "node dist/index.js"

src/db/schema/customers.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { int, sqliteTable, text } from "drizzle-orm/sqlite-core";
2+
3+
export const customersTable = sqliteTable("customers", {
4+
id: int().primaryKey({ autoIncrement: true }),
5+
firstName: text().notNull(),
6+
lastName: text().notNull(),
7+
email: text().notNull().unique(),
8+
code: text().notNull().unique(),
9+
});

src/db/schema/subscriptions.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { int, sqliteTable, text } from "drizzle-orm/sqlite-core";
2+
import { customersTable } from "./customers.js";
3+
import { plansTable } from "./plans.js";
4+
5+
export const subscriptionsTable = sqliteTable("subscriptions", {
6+
id: int().primaryKey({ autoIncrement: true }),
7+
code: text().notNull(),
8+
status: text().notNull(),
9+
plan: int().notNull().references(() => plansTable.id),
10+
customer: int().notNull().references(() => customersTable.id),
11+
emailToken: text().notNull(),
12+
nextPaymentDate: text().notNull()
13+
});

src/db/schema/transactions.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { int, sqliteTable, text } from "drizzle-orm/sqlite-core";
2+
import { plansTable } from "./plans.js";
3+
import { customersTable } from "./customers.js";
4+
5+
export const transactionsTable = sqliteTable("transactions", {
6+
id: int().primaryKey({ autoIncrement: true }),
7+
amount: int().notNull().default(0),
8+
reference: text().notNull(),
9+
plan: int().references(() => plansTable.id),
10+
customer: int().references(() => customersTable.id),
11+
});

src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import plansRouter from './routes/plans.js'
44
import { showRoutes } from 'hono/dev'
55
import transactionRouter from './routes/transaction.js'
66
import webhookRouter from './routes/webhooks.js'
7+
import customersRouter from './routes/customer.js'
8+
import subscriptionsRouter from './routes/subscriptions.js'
79

810
const app = new Hono()
911

@@ -12,15 +14,17 @@ app.get('/', (c) => {
1214
})
1315

1416
app.route("/plan", plansRouter)
17+
app.route("/customer", customersRouter)
1518
app.route("/transaction", transactionRouter)
19+
app.route("/subscription", subscriptionsRouter)
1620
app.route("/webhook", webhookRouter)
1721

1822
// To list all routes
1923
showRoutes(app)
2024

2125
serve({
2226
fetch: app.fetch,
23-
port: 3000
27+
port: 3030
2428
}, (info) => {
2529
console.log(`Server is running on http://localhost:${info.port}`)
2630
})

src/routes/customer.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import 'dotenv/config';
2+
import { Hono } from "hono";
3+
import { db } from '../db.js';
4+
import { zValidator } from '@hono/zod-validator';
5+
import { Customer, CustomerUpdate } from '../schema/index.js';
6+
import { eq } from 'drizzle-orm';
7+
import { customersTable } from '../db/schema/customers.js';
8+
9+
const customersRouter = new Hono()
10+
11+
customersRouter.post("/insert", async (c) => {
12+
const data = await c.req.json()
13+
14+
const {
15+
first_name,
16+
last_name,
17+
email,
18+
code
19+
} = data
20+
21+
const customer = await db.insert(customersTable)
22+
.values({
23+
firstName: first_name,
24+
lastName: last_name,
25+
email: email,
26+
code: code
27+
})
28+
.returning()
29+
30+
return c.json({
31+
status: true,
32+
data: customer[0]
33+
}, 201)
34+
})
35+
36+
customersRouter.get("/", async (c) => {
37+
const customers = await db.select().from(customersTable)
38+
39+
return c.json({
40+
status: true,
41+
data: customers,
42+
}, 200)
43+
})
44+
45+
customersRouter.post("/",
46+
zValidator('json', Customer),
47+
async (c) => {
48+
const validateReq = c.req.valid('json')
49+
const api = await fetch(`${process.env.PAYSTACK_BASE_URL}/customer`, {
50+
method: "POST",
51+
headers: {
52+
"Authorization": `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
53+
'Content-Type': "application/json"
54+
},
55+
body: JSON.stringify(validateReq)
56+
})
57+
58+
const data = await api.json()
59+
60+
if (!data["status"]) {
61+
return c.json({
62+
status: false,
63+
data: {}
64+
}, 500)
65+
}
66+
67+
const {
68+
first_name,
69+
last_name,
70+
email,
71+
customer_code
72+
} = data["data"]
73+
74+
const customer = await db.insert(customersTable)
75+
.values({
76+
firstName: first_name,
77+
lastName: last_name,
78+
email: email,
79+
code: customer_code
80+
})
81+
.returning()
82+
83+
return c.json({
84+
status: true,
85+
data: customer[0]
86+
}, 201)
87+
}
88+
)
89+
90+
customersRouter.put("/:id",
91+
zValidator('json', CustomerUpdate),
92+
async (c) => {
93+
const validatedReq = c.req.valid('json')
94+
const id = c.req.param('id')
95+
96+
const customer = await db.update(customersTable)
97+
.set({ ...validatedReq })
98+
.where(eq(customersTable.id, parseInt(id)))
99+
.returning();
100+
101+
return c.json({
102+
status: true,
103+
data: customer[0]
104+
}, 200)
105+
}
106+
)
107+
108+
export default customersRouter

src/routes/subscriptions.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import 'dotenv/config';
2+
import { Hono } from "hono";
3+
import { db } from '../db.js';
4+
import { zValidator } from '@hono/zod-validator';
5+
import { SubscriptionCreate } from '../schema/index.js';
6+
import { eq } from 'drizzle-orm';
7+
import { subscriptionsTable } from '../db/schema/subscriptions.js';
8+
import { plansTable } from '../db/schema/plans.js';
9+
import { customersTable } from '../db/schema/customers.js';
10+
11+
const subscriptionsRouter = new Hono()
12+
13+
subscriptionsRouter.get("/", async (c) => {
14+
const subscriptions = await db.select().from(subscriptionsTable)
15+
16+
return c.json({
17+
status: true,
18+
data: subscriptions,
19+
}, 200)
20+
})
21+
22+
subscriptionsRouter.post("/",
23+
zValidator('json', SubscriptionCreate),
24+
async (c) => {
25+
const validateReq = c.req.valid('json')
26+
const api = await fetch(`${process.env.PAYSTACK_BASE_URL}/subscription`, {
27+
method: "POST",
28+
headers: {
29+
"Authorization": `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
30+
'Content-Type': "application/json"
31+
},
32+
body: JSON.stringify(validateReq)
33+
})
34+
35+
const data = await api.json()
36+
37+
if (!data["status"]) {
38+
return c.json({
39+
status: false,
40+
data: {}
41+
}, 500)
42+
}
43+
44+
const {
45+
subscription_code,
46+
status,
47+
plan: planCode,
48+
customer: customerCode,
49+
email_token,
50+
next_payment_date
51+
} = data["data"]
52+
const plan = await db.select({ id: plansTable.id })
53+
.from(plansTable)
54+
.where(eq(plansTable.code, planCode))
55+
const customer = await db.select({ id: customersTable.id })
56+
.from(customersTable)
57+
.where(eq(customerCode.code, customerCode))
58+
59+
const subscription = await db.insert(subscriptionsTable)
60+
.values({
61+
code: subscription_code,
62+
status: status,
63+
emailToken: email_token,
64+
plan: plan[0].id,
65+
customer: customer[0].id,
66+
nextPaymentDate: next_payment_date
67+
})
68+
.returning()
69+
70+
return c.json({
71+
status: true,
72+
data: subscription[0]
73+
}, 201)
74+
}
75+
)
76+
77+
78+
export default subscriptionsRouter

0 commit comments

Comments
 (0)