create-charcole 2.2.1 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -3
- package/README.md +94 -4
- package/bin/index.js +174 -4
- package/package.json +3 -3
- package/packages/payments/CHANGELOG.md +14 -0
- package/packages/payments/README.md +222 -0
- package/packages/payments/charcoles-payments-1.0.0.tgz +0 -0
- package/packages/payments/package.json +61 -0
- package/packages/payments/smoke-test.js +20 -0
- package/packages/payments/src/__tests__/LemonSqueezyAdapter.test.js +236 -0
- package/packages/payments/src/__tests__/StripeAdapter.test.js +139 -0
- package/packages/payments/src/__tests__/payments.service.test.js +131 -0
- package/packages/payments/src/__tests__/webhookUtils.test.js +47 -0
- package/packages/payments/src/adapters/LemonSqueezyAdapter.js +150 -0
- package/packages/payments/src/adapters/PaymentAdapter.js +109 -0
- package/packages/payments/src/adapters/StripeAdapter.js +114 -0
- package/packages/payments/src/controllers/payments.controller.js +48 -0
- package/packages/payments/src/errors/PaymentError.js +8 -0
- package/packages/payments/src/helpers/webhookUtils.js +27 -0
- package/packages/payments/src/index.d.ts +87 -0
- package/packages/payments/src/index.js +6 -0
- package/packages/payments/src/routes/payments.routes.js +41 -0
- package/packages/payments/src/schemas/payments.schemas.js +24 -0
- package/packages/payments/src/services/payments.service.js +68 -0
- package/packages/swagger/BACKWARD_COMPATIBILITY.md +1 -1
- package/packages/swagger/CHANGELOG.md +1 -1
- package/packages/swagger/README.md +3 -3
- package/packages/swagger/package.json +3 -3
- package/packages/swagger/src/setup.js +1 -1
- package/plan-2.3.0.md +1756 -0
- package/template/js/.env.example +20 -1
- package/template/js/README.md +140 -8
- package/template/js/basePackage.json +1 -1
- package/template/js/src/app.js +18 -1
- package/template/js/src/config/env.js +8 -0
- package/template/js/src/config/swagger.config.js +1 -1
- package/template/js/src/lib/swagger/SWAGGER_GUIDE.md +37 -3
- package/template/js/src/modules/payments/__tests__/payments.controller.test.js +342 -0
- package/template/js/src/modules/payments/__tests__/payments.routes.test.js +256 -0
- package/template/js/src/modules/payments/__tests__/payments.schemas.test.js +94 -0
- package/template/js/src/modules/payments/__tests__/payments.service.test.js +141 -0
- package/template/js/src/modules/payments/package.json +7 -0
- package/template/js/src/modules/payments/payments.adapter.js +47 -0
- package/template/js/src/modules/payments/payments.constants.js +20 -0
- package/template/js/src/modules/payments/payments.controller.js +85 -0
- package/template/js/src/modules/payments/payments.routes.js +125 -0
- package/template/js/src/modules/payments/payments.schemas.js +28 -0
- package/template/js/src/modules/payments/payments.service.js +34 -0
- package/template/js/src/modules/swagger/package.json +1 -1
- package/template/js/src/routes/index.js +16 -0
- package/template/ts/.env.example +18 -1
- package/template/ts/README.md +142 -8
- package/template/ts/basePackage.json +1 -1
- package/template/ts/src/app.ts +13 -0
- package/template/ts/src/config/env.ts +7 -0
- package/template/ts/src/config/swagger.config.ts +1 -1
- package/template/ts/src/lib/swagger/SWAGGER_GUIDE.md +36 -2
- package/template/ts/src/modules/payments/__tests__/payments.controller.test.ts +282 -0
- package/template/ts/src/modules/payments/__tests__/payments.routes.test.ts +256 -0
- package/template/ts/src/modules/payments/__tests__/payments.schemas.test.ts +94 -0
- package/template/ts/src/modules/payments/__tests__/payments.service.test.ts +135 -0
- package/template/ts/src/modules/payments/package.json +7 -0
- package/template/ts/src/modules/payments/payments.adapter.ts +74 -0
- package/template/ts/src/modules/payments/payments.constants.ts +18 -0
- package/template/ts/src/modules/payments/payments.controller.ts +104 -0
- package/template/ts/src/modules/payments/payments.routes.ts +125 -0
- package/template/ts/src/modules/payments/payments.schemas.ts +31 -0
- package/template/ts/src/modules/payments/payments.service.ts +51 -0
- package/template/ts/src/modules/payments/payments.types.ts +56 -0
- package/template/ts/src/modules/swagger/package.json +1 -1
- package/template/ts/src/routes/index.ts +8 -0
- package/packages/swagger/package-lock.json +0 -1715
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { validateRequest } from "../../middlewares/validateRequest.js";
|
|
3
|
+
import * as controller from "./payments.controller.js";
|
|
4
|
+
import {
|
|
5
|
+
createPaymentSchema,
|
|
6
|
+
refundPaymentSchema,
|
|
7
|
+
} from "./payments.schemas.js";
|
|
8
|
+
|
|
9
|
+
const router = Router();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @swagger
|
|
13
|
+
* /api/payments/create-intent:
|
|
14
|
+
* post:
|
|
15
|
+
* summary: Create a payment intent or checkout session
|
|
16
|
+
* tags:
|
|
17
|
+
* - Payments
|
|
18
|
+
* requestBody:
|
|
19
|
+
* required: true
|
|
20
|
+
* content:
|
|
21
|
+
* application/json:
|
|
22
|
+
* schema:
|
|
23
|
+
* type: object
|
|
24
|
+
* required: [amount, currency]
|
|
25
|
+
* properties:
|
|
26
|
+
* amount:
|
|
27
|
+
* type: integer
|
|
28
|
+
* description: Amount in smallest currency unit (cents for USD, paisas for PKR)
|
|
29
|
+
* example: 2999
|
|
30
|
+
* currency:
|
|
31
|
+
* type: string
|
|
32
|
+
* description: ISO 4217 currency code
|
|
33
|
+
* example: usd
|
|
34
|
+
* metadata:
|
|
35
|
+
* type: object
|
|
36
|
+
* description: Optional metadata. LemonSqueezy requires variantId here.
|
|
37
|
+
* responses:
|
|
38
|
+
* 201:
|
|
39
|
+
* description: Payment intent created
|
|
40
|
+
* 400:
|
|
41
|
+
* description: Validation error
|
|
42
|
+
*/
|
|
43
|
+
router.post(
|
|
44
|
+
"/create-intent",
|
|
45
|
+
validateRequest(createPaymentSchema),
|
|
46
|
+
controller.createPayment,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @swagger
|
|
51
|
+
* /api/payments/refund:
|
|
52
|
+
* post:
|
|
53
|
+
* summary: Refund a payment
|
|
54
|
+
* tags:
|
|
55
|
+
* - Payments
|
|
56
|
+
* requestBody:
|
|
57
|
+
* required: true
|
|
58
|
+
* content:
|
|
59
|
+
* application/json:
|
|
60
|
+
* schema:
|
|
61
|
+
* type: object
|
|
62
|
+
* required: [paymentId]
|
|
63
|
+
* properties:
|
|
64
|
+
* paymentId:
|
|
65
|
+
* type: string
|
|
66
|
+
* example: pi_123456789
|
|
67
|
+
* amount:
|
|
68
|
+
* type: integer
|
|
69
|
+
* description: Optional refund amount in smallest currency unit
|
|
70
|
+
* example: 2999
|
|
71
|
+
* responses:
|
|
72
|
+
* 200:
|
|
73
|
+
* description: Refund processed
|
|
74
|
+
* 400:
|
|
75
|
+
* description: Validation error
|
|
76
|
+
*/
|
|
77
|
+
router.post(
|
|
78
|
+
"/refund",
|
|
79
|
+
validateRequest(refundPaymentSchema),
|
|
80
|
+
controller.refundPayment,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @swagger
|
|
85
|
+
* /api/payments/status/{paymentId}:
|
|
86
|
+
* get:
|
|
87
|
+
* summary: Get payment status
|
|
88
|
+
* tags:
|
|
89
|
+
* - Payments
|
|
90
|
+
* parameters:
|
|
91
|
+
* - in: path
|
|
92
|
+
* name: paymentId
|
|
93
|
+
* required: true
|
|
94
|
+
* schema:
|
|
95
|
+
* type: string
|
|
96
|
+
* responses:
|
|
97
|
+
* 200:
|
|
98
|
+
* description: Payment status retrieved
|
|
99
|
+
* 404:
|
|
100
|
+
* description: Payment not found
|
|
101
|
+
*/
|
|
102
|
+
router.get("/status/:paymentId", controller.getPaymentStatus);
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @swagger
|
|
106
|
+
* /api/payments/webhook:
|
|
107
|
+
* post:
|
|
108
|
+
* summary: Receive payment provider webhook events
|
|
109
|
+
* tags:
|
|
110
|
+
* - Payments
|
|
111
|
+
* requestBody:
|
|
112
|
+
* required: true
|
|
113
|
+
* content:
|
|
114
|
+
* application/json:
|
|
115
|
+
* schema:
|
|
116
|
+
* type: object
|
|
117
|
+
* responses:
|
|
118
|
+
* 200:
|
|
119
|
+
* description: Webhook received
|
|
120
|
+
* 400:
|
|
121
|
+
* description: Missing signature header
|
|
122
|
+
*/
|
|
123
|
+
router.post("/webhook", controller.handleWebhook);
|
|
124
|
+
|
|
125
|
+
export default router;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const createPaymentSchema = z.object({
|
|
4
|
+
amount: z
|
|
5
|
+
.number({ required_error: "amount is required" })
|
|
6
|
+
.int("amount must be an integer (smallest currency unit, e.g. cents)")
|
|
7
|
+
.positive("amount must be positive")
|
|
8
|
+
.max(99999999, "amount exceeds maximum"),
|
|
9
|
+
|
|
10
|
+
currency: z
|
|
11
|
+
.string({ required_error: "currency is required" })
|
|
12
|
+
.length(3, "currency must be a 3-letter ISO 4217 code (e.g. usd, pkr)")
|
|
13
|
+
.toLowerCase(),
|
|
14
|
+
|
|
15
|
+
metadata: z.record(z.string()).optional().default({}),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const refundPaymentSchema = z.object({
|
|
19
|
+
paymentId: z
|
|
20
|
+
.string({ required_error: "paymentId is required" })
|
|
21
|
+
.min(1, "paymentId cannot be empty"),
|
|
22
|
+
|
|
23
|
+
amount: z
|
|
24
|
+
.number()
|
|
25
|
+
.int("amount must be an integer")
|
|
26
|
+
.positive("amount must be positive")
|
|
27
|
+
.optional(),
|
|
28
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { getAdapter } from "./payments.adapter.js";
|
|
2
|
+
|
|
3
|
+
const processedWebhookIds = new Set();
|
|
4
|
+
|
|
5
|
+
export async function createPayment({ amount, currency, metadata }) {
|
|
6
|
+
const adapter = getAdapter();
|
|
7
|
+
return adapter.createPayment({ amount, currency, metadata });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function refundPayment({ paymentId, amount }) {
|
|
11
|
+
const adapter = getAdapter();
|
|
12
|
+
return adapter.refundPayment({ paymentId, amount });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function getPaymentStatus(paymentId) {
|
|
16
|
+
const adapter = getAdapter();
|
|
17
|
+
return adapter.getPaymentStatus(paymentId);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function processWebhook(rawBody, signature) {
|
|
21
|
+
const adapter = getAdapter();
|
|
22
|
+
const result = await adapter.verifyWebhook(rawBody, signature);
|
|
23
|
+
|
|
24
|
+
const eventId = result.data?.id
|
|
25
|
+
? String(result.data.id)
|
|
26
|
+
: `${result.event}-${Date.now()}`;
|
|
27
|
+
|
|
28
|
+
if (processedWebhookIds.has(eventId)) {
|
|
29
|
+
return { ...result, duplicate: true };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
processedWebhookIds.add(eventId);
|
|
33
|
+
return { ...result, duplicate: false };
|
|
34
|
+
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { Router } from "express";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
2
5
|
import {
|
|
3
6
|
getHealth,
|
|
4
7
|
createItem,
|
|
@@ -8,6 +11,9 @@ import { validateRequest } from "../middlewares/validateRequest.js";
|
|
|
8
11
|
import { requireAuth } from "../modules/auth/auth.middlewares.js";
|
|
9
12
|
import protectedRoutes from "./protected.js";
|
|
10
13
|
import authRoutes from "../modules/auth/auth.routes.js";
|
|
14
|
+
import paymentsRoutes from "../modules/payments/payments.routes.js";
|
|
15
|
+
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
17
|
const router = Router();
|
|
12
18
|
|
|
13
19
|
// Health check
|
|
@@ -19,6 +25,16 @@ router.post("/items", validateRequest(createItemSchema), createItem);
|
|
|
19
25
|
// 🔐 Auth routes
|
|
20
26
|
router.use("/auth", authRoutes);
|
|
21
27
|
|
|
28
|
+
// Payments routes — only loaded if the payments module was included during scaffolding
|
|
29
|
+
const paymentsRoutesPath = join(
|
|
30
|
+
__dirname,
|
|
31
|
+
"../modules/payments/payments.routes.js",
|
|
32
|
+
);
|
|
33
|
+
if (existsSync(paymentsRoutesPath)) {
|
|
34
|
+
const { default: paymentsRoutes } = await import(paymentsRoutesPath);
|
|
35
|
+
router.use("/payments", paymentsRoutes);
|
|
36
|
+
}
|
|
37
|
+
|
|
22
38
|
// 🔐 Protected routes (REQUIRED BEARER TOKEN FOR THEM)
|
|
23
39
|
router.use("/protected", protectedRoutes);
|
|
24
40
|
|
package/template/ts/.env.example
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# Server Configuration
|
|
2
|
+
APP_NAME=CHARCOLE API
|
|
2
3
|
NODE_ENV=development
|
|
3
4
|
PORT=3000
|
|
4
5
|
|
|
@@ -13,4 +14,20 @@ REQUEST_TIMEOUT=30000
|
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
# Authentication
|
|
16
|
-
JWT_SECRET=your-secret-key-here
|
|
17
|
+
JWT_SECRET=your-secret-key-here
|
|
18
|
+
|
|
19
|
+
# ─── Payments ──────────────────────────────────────────────────────────────────
|
|
20
|
+
# PAYMENT_PROVIDER selects the active payment adapter.
|
|
21
|
+
# Options: "stripe" | "lemonsqueezy"
|
|
22
|
+
# Use "lemonsqueezy" if you are in Pakistan — Stripe does not support PKR payouts.
|
|
23
|
+
PAYMENT_PROVIDER=
|
|
24
|
+
|
|
25
|
+
# Stripe — https://dashboard.stripe.com/apikeys
|
|
26
|
+
STRIPE_SECRET_KEY=
|
|
27
|
+
STRIPE_WEBHOOK_SECRET=
|
|
28
|
+
STRIPE_PUBLISHABLE_KEY=
|
|
29
|
+
|
|
30
|
+
# LemonSqueezy — https://app.lemonsqueezy.com/settings/api
|
|
31
|
+
LEMONSQUEEZY_API_KEY=
|
|
32
|
+
LEMONSQUEEZY_WEBHOOK_SECRET=
|
|
33
|
+
LEMONSQUEEZY_STORE_ID=
|
package/template/ts/README.md
CHANGED
|
@@ -9,11 +9,12 @@ Welcome! This guide will help you set up and start using the Charcole API framew
|
|
|
9
9
|
3. [Configuration](#configuration)
|
|
10
10
|
4. [Creating Your First Endpoint](#creating-your-first-endpoint)
|
|
11
11
|
5. [API Documentation with Swagger](#api-documentation-with-swagger)
|
|
12
|
-
6. [
|
|
13
|
-
7. [
|
|
14
|
-
8. [
|
|
15
|
-
9. [
|
|
16
|
-
10. [
|
|
12
|
+
6. [Payment Processing (if enabled)](#payment-processing-if-enabled)
|
|
13
|
+
7. [Error Handling](#error-handling)
|
|
14
|
+
8. [Validation](#validation)
|
|
15
|
+
9. [Logging](#logging)
|
|
16
|
+
10. [Running Your API](#running-your-api)
|
|
17
|
+
11. [Troubleshooting](#troubleshooting)
|
|
17
18
|
|
|
18
19
|
---
|
|
19
20
|
|
|
@@ -34,9 +35,13 @@ Welcome! This guide will help you set up and start using the Charcole API framew
|
|
|
34
35
|
|
|
35
36
|
2. **Create environment file**
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
The `create-charcole` CLI will automatically create a `.env` from `.env.example` and initialize a Git repository for you. Edit the generated `.env` as needed.
|
|
39
|
+
|
|
40
|
+
If you prefer to create it manually:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
cp .env.example .env
|
|
44
|
+
```
|
|
40
45
|
|
|
41
46
|
3. **Run the charcole**
|
|
42
47
|
```bash
|
|
@@ -519,6 +524,135 @@ For protected endpoints:
|
|
|
519
524
|
|
|
520
525
|
---
|
|
521
526
|
|
|
527
|
+
## 💳 Payment Processing (if enabled)
|
|
528
|
+
|
|
529
|
+
### Overview
|
|
530
|
+
|
|
531
|
+
Your API comes with **production-ready payment processing** if you selected the payments module during setup. Choose between:
|
|
532
|
+
|
|
533
|
+
- **Stripe** - Industry standard payment processing
|
|
534
|
+
- **LemonSqueezy** - Perfect for Pakistani developers (PKR payout support via bank transfer)
|
|
535
|
+
- **Both** - Flexibility to switch providers
|
|
536
|
+
|
|
537
|
+
### Payment Endpoints
|
|
538
|
+
|
|
539
|
+
Four ready-to-use payment APIs are auto-configured:
|
|
540
|
+
|
|
541
|
+
```
|
|
542
|
+
POST /api/payments/create-intent # Create payment intent
|
|
543
|
+
POST /api/payments/refund # Refund a payment
|
|
544
|
+
GET /api/payments/status/:paymentId # Check payment status
|
|
545
|
+
POST /api/payments/webhook # Webhook receiver
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
### Configuration
|
|
549
|
+
|
|
550
|
+
Add payment provider credentials to `.env`:
|
|
551
|
+
|
|
552
|
+
```env
|
|
553
|
+
# Payment Provider (stripe, lemonsqueezy, or both)
|
|
554
|
+
PAYMENT_PROVIDER=stripe
|
|
555
|
+
|
|
556
|
+
# Stripe Configuration
|
|
557
|
+
STRIPE_SECRET_KEY=sk_live_...
|
|
558
|
+
STRIPE_WEBHOOK_SECRET=whsec_...
|
|
559
|
+
|
|
560
|
+
# LemonSqueezy Configuration
|
|
561
|
+
LEMONSQUEEZY_API_KEY=...
|
|
562
|
+
LEMONSQUEEZY_WEBHOOK_SECRET=...
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
### Usage Example
|
|
566
|
+
|
|
567
|
+
```typescript
|
|
568
|
+
import { setupPayments } from "@charcoles/payments";
|
|
569
|
+
|
|
570
|
+
// In your app.ts
|
|
571
|
+
const paymentAdapter = setupPayments({
|
|
572
|
+
provider: process.env.PAYMENT_PROVIDER,
|
|
573
|
+
stripeKey: process.env.STRIPE_SECRET_KEY,
|
|
574
|
+
lemonsqueezyKey: process.env.LEMONSQUEEZY_API_KEY,
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
// In your controller
|
|
578
|
+
export const createPaymentIntent = asyncHandler(
|
|
579
|
+
async (req: Request, res: Response) => {
|
|
580
|
+
const { amount, currency, customerId } = req.body;
|
|
581
|
+
|
|
582
|
+
const intent = await paymentAdapter.createPayment({
|
|
583
|
+
amount,
|
|
584
|
+
currency,
|
|
585
|
+
customerId,
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
sendSuccess(res, intent, 201, "Payment intent created");
|
|
589
|
+
},
|
|
590
|
+
);
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
### Webhook Handling
|
|
594
|
+
|
|
595
|
+
Webhooks are automatically configured and validated:
|
|
596
|
+
|
|
597
|
+
```typescript
|
|
598
|
+
// src/modules/payments/payments.routes.ts
|
|
599
|
+
// POST /api/payments/webhook automatically handles:
|
|
600
|
+
// - Stripe: payment_intent.succeeded, charge.refunded
|
|
601
|
+
// - LemonSqueezy: order_created, order_refunded
|
|
602
|
+
|
|
603
|
+
// Raw body middleware auto-configured in app.ts
|
|
604
|
+
// app.use('/payments/webhook', express.raw({ type: 'application/json' }))
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
### Error Handling
|
|
608
|
+
|
|
609
|
+
Payment errors are automatically caught by the global error handler:
|
|
610
|
+
|
|
611
|
+
```typescript
|
|
612
|
+
import { PaymentError } from "@charcoles/payments";
|
|
613
|
+
|
|
614
|
+
// Throws structured error
|
|
615
|
+
throw new PaymentError("Insufficient funds", {
|
|
616
|
+
code: "insufficient_funds",
|
|
617
|
+
statusCode: 402,
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
// Response:
|
|
621
|
+
// {
|
|
622
|
+
// "success": false,
|
|
623
|
+
// "message": "Insufficient funds",
|
|
624
|
+
// "statusCode": 402,
|
|
625
|
+
// "code": "insufficient_funds"
|
|
626
|
+
// }
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
### Testing Payments Locally
|
|
630
|
+
|
|
631
|
+
**Stripe:**
|
|
632
|
+
|
|
633
|
+
```bash
|
|
634
|
+
STRIPE_SECRET_KEY=sk_test_... npm run dev
|
|
635
|
+
# Use test card: 4242 4242 4242 4242
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
**LemonSqueezy:**
|
|
639
|
+
|
|
640
|
+
```bash
|
|
641
|
+
LEMONSQUEEZY_API_KEY=... npm run dev
|
|
642
|
+
# Sandbox mode automatically used with test keys
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
### Documentation in Swagger
|
|
646
|
+
|
|
647
|
+
All payment endpoints are automatically documented in Swagger UI when enabled:
|
|
648
|
+
|
|
649
|
+
1. Start server: `npm run dev`
|
|
650
|
+
2. Visit http://localhost:3000/api-docs
|
|
651
|
+
3. Look for **Payments** tag
|
|
652
|
+
4. Test all endpoints directly from the browser
|
|
653
|
+
|
|
654
|
+
---
|
|
655
|
+
|
|
522
656
|
## ✔️ Validation
|
|
523
657
|
|
|
524
658
|
### Zod Schema Basics
|
package/template/ts/src/app.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import express, { Request, Response, NextFunction } from "express";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
2
5
|
import cors from "cors";
|
|
3
6
|
import { userRepo } from "./repositories/user.repo.ts";
|
|
4
7
|
import { env } from "./config/env.ts";
|
|
@@ -14,6 +17,12 @@ import routes from "./routes/index.ts";
|
|
|
14
17
|
import swaggerOptions from "./config/swagger.config.ts";
|
|
15
18
|
import { setupSwagger } from "@charcoles/swagger";
|
|
16
19
|
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const paymentsModulePath = join(
|
|
22
|
+
__dirname,
|
|
23
|
+
"modules/payments/payments.routes.ts",
|
|
24
|
+
);
|
|
25
|
+
|
|
17
26
|
export const app = express();
|
|
18
27
|
|
|
19
28
|
app.set("trust proxy", 1);
|
|
@@ -27,6 +36,10 @@ app.use(
|
|
|
27
36
|
}),
|
|
28
37
|
);
|
|
29
38
|
|
|
39
|
+
if (existsSync(paymentsModulePath)) {
|
|
40
|
+
app.use("/payments/webhook", express.raw({ type: "application/json" }));
|
|
41
|
+
}
|
|
42
|
+
|
|
30
43
|
app.use(express.json({ limit: "10mb" }));
|
|
31
44
|
app.use(express.urlencoded({ extended: true, limit: "10mb" }));
|
|
32
45
|
|
|
@@ -8,6 +8,13 @@ const envSchema = z.object({
|
|
|
8
8
|
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
|
|
9
9
|
CORS_ORIGIN: z.string().default("*"),
|
|
10
10
|
REQUEST_TIMEOUT: z.coerce.number().default(30000),
|
|
11
|
+
PAYMENT_PROVIDER: z.enum(["stripe", "lemonsqueezy"]).optional(),
|
|
12
|
+
STRIPE_SECRET_KEY: z.string().optional(),
|
|
13
|
+
STRIPE_WEBHOOK_SECRET: z.string().optional(),
|
|
14
|
+
STRIPE_PUBLISHABLE_KEY: z.string().optional(),
|
|
15
|
+
LEMONSQUEEZY_API_KEY: z.string().optional(),
|
|
16
|
+
LEMONSQUEEZY_WEBHOOK_SECRET: z.string().optional(),
|
|
17
|
+
LEMONSQUEEZY_STORE_ID: z.string().optional(),
|
|
11
18
|
});
|
|
12
19
|
|
|
13
20
|
type EnvSchema = z.infer<typeof envSchema>;
|
|
@@ -3,7 +3,7 @@ import { createItemSchema } from "../modules/health/controller.ts";
|
|
|
3
3
|
|
|
4
4
|
const swaggerConfig = {
|
|
5
5
|
title: process.env.APP_NAME || "Charcole API",
|
|
6
|
-
version: process.env.APP_VERSION || "1.0.
|
|
6
|
+
version: process.env.APP_VERSION || "1.0.1",
|
|
7
7
|
description: "Production-ready Node.js Express API",
|
|
8
8
|
path: "/api-docs",
|
|
9
9
|
servers: [
|
|
@@ -18,7 +18,7 @@ import { createItemSchema } from "../modules/health/controller.ts";
|
|
|
18
18
|
|
|
19
19
|
const swaggerConfig = {
|
|
20
20
|
title: "My API",
|
|
21
|
-
version: "1.0.
|
|
21
|
+
version: "1.0.1",
|
|
22
22
|
// Auto-register schemas - they'll be converted to OpenAPI automatically!
|
|
23
23
|
schemas: {
|
|
24
24
|
registerSchema,
|
|
@@ -96,6 +96,40 @@ That's it! Your Zod schemas are now available as `$ref` in Swagger.
|
|
|
96
96
|
|
|
97
97
|
**Result:** 60 lines eliminated! And your schema stays in sync automatically.
|
|
98
98
|
|
|
99
|
+
### Example 2.1: Documenting a Payments Webhook Endpoint
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
/**
|
|
103
|
+
* @swagger
|
|
104
|
+
* /api/payments/webhook:
|
|
105
|
+
* post:
|
|
106
|
+
* summary: Receive payment provider webhook events
|
|
107
|
+
* tags:
|
|
108
|
+
* - Payments
|
|
109
|
+
* requestBody:
|
|
110
|
+
* required: true
|
|
111
|
+
* content:
|
|
112
|
+
* application/json:
|
|
113
|
+
* schema:
|
|
114
|
+
* type: object
|
|
115
|
+
* responses:
|
|
116
|
+
* 200:
|
|
117
|
+
* description: Webhook received
|
|
118
|
+
* 400:
|
|
119
|
+
* description: Missing signature header
|
|
120
|
+
*/
|
|
121
|
+
router.post("/webhook", handleWebhook);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
When you expose a webhook route, mount raw JSON middleware before `express.json()` so the provider signature verification receives the raw body:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
app.use("/payments/webhook", express.raw({ type: "application/json" }));
|
|
128
|
+
app.use(express.json());
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
This means your final webhook route becomes `/api/payments/webhook` when the payments router is mounted at `/api`.
|
|
132
|
+
|
|
99
133
|
---
|
|
100
134
|
|
|
101
135
|
## 📚 Complete Examples
|
|
@@ -472,7 +506,7 @@ const app = express();
|
|
|
472
506
|
|
|
473
507
|
setupSwagger(app, {
|
|
474
508
|
title: "My API",
|
|
475
|
-
version: "1.0.
|
|
509
|
+
version: "1.0.1",
|
|
476
510
|
schemas: {
|
|
477
511
|
mySchema, // Your Zod schemas
|
|
478
512
|
},
|