create-charcole 2.2.2 → 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 +33 -2
- package/README.md +92 -5
- package/bin/index.js +121 -1
- package/package.json +1 -1
- 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/plan-2.3.0.md +1756 -0
- package/template/js/.env.example +19 -1
- package/template/js/README.md +133 -5
- package/template/js/basePackage.json +1 -1
- package/template/js/src/app.js +17 -0
- package/template/js/src/config/env.js +8 -0
- package/template/js/src/lib/swagger/SWAGGER_GUIDE.md +34 -0
- 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 +17 -1
- package/template/ts/README.md +135 -5
- 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/lib/swagger/SWAGGER_GUIDE.md +34 -0
- 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/CHANGELOG.md
CHANGED
|
@@ -5,9 +5,40 @@ All notable changes to Charcole will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [2.
|
|
8
|
+
## [2.3.0] – 2026-05-05
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Payments module** — optional, production-ready payment processing for scaffolded projects
|
|
13
|
+
- `@charcoles/payments` standalone npm package — drop-in payment support for any Express app
|
|
14
|
+
- **Stripe adapter** — full payment intent lifecycle: create, refund, status check, webhook verification
|
|
15
|
+
- **LemonSqueezy adapter** — checkout sessions, order retrieval, refunds, and webhook HMAC verification
|
|
16
|
+
- Added specifically for Pakistani developers: Stripe does not support PKR payouts; LemonSqueezy provides merchant-of-record payments with full Pakistani bank payout support
|
|
17
|
+
- Adapter pattern for provider abstraction — switch between Stripe and LemonSqueezy via `PAYMENT_PROVIDER` env var
|
|
18
|
+
- New CLI prompt: "Include payments module?" with provider selection (Stripe / LemonSqueezy / Both)
|
|
19
|
+
- `src/modules/payments/` module in both JS and TS templates with:
|
|
20
|
+
- `payments.adapter.js/ts` — provider factory with singleton caching
|
|
21
|
+
- `payments.service.js/ts` — service layer with in-memory webhook deduplication
|
|
22
|
+
- `payments.controller.js/ts` — request handlers for all 4 endpoints
|
|
23
|
+
- `payments.routes.js/ts` — Express router with Swagger JSDoc comments
|
|
24
|
+
- `payments.schemas.js/ts` — Zod validation schemas for all request bodies
|
|
25
|
+
- `payments.constants.js/ts` — provider names, event names, webhook header names
|
|
26
|
+
- Four new API endpoints in scaffolded projects:
|
|
27
|
+
- `POST /payments/create-intent` — create Stripe PaymentIntent or LemonSqueezy checkout session
|
|
28
|
+
- `POST /payments/refund` — full or partial refunds
|
|
29
|
+
- `GET /payments/status/:paymentId` — normalized payment status across providers
|
|
30
|
+
- `POST /payments/webhook` — secure webhook event handling with signature verification
|
|
31
|
+
- Webhook raw body middleware auto-configured in `app.js` when payments module is selected
|
|
32
|
+
- Conditional route loading for payments (same `existsSync` pattern as auth and swagger)
|
|
33
|
+
- Payment env vars added to Zod env schema in both templates (all optional)
|
|
34
|
+
- Payment section added to `.env.example` in both templates with inline documentation
|
|
35
|
+
- Swagger JSDoc comments on all 4 payment endpoints
|
|
36
|
+
- `## Payments Module` section added to `SWAGGER_GUIDE.md` in both templates
|
|
37
|
+
- Payments section added to root `README.md` including migration guide for existing projects
|
|
38
|
+
- Integration tests for payments controller, service, schemas, and routes in both JS and TS templates
|
|
39
|
+
- `PaymentError` custom error class with `code` and `statusCode` fields, compatible with existing error handler
|
|
9
40
|
|
|
10
|
-
|
|
41
|
+
## [2.2.2] – 2026-02-06
|
|
11
42
|
|
|
12
43
|
#### ✨ **New Features**
|
|
13
44
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Charcole API v2.
|
|
1
|
+
# Charcole API v2.3
|
|
2
2
|
|
|
3
|
-
> **Charcole v2.
|
|
3
|
+
> **Charcole v2.3 is a production-grade Node.js backend starter CLI that scaffolds enterprise-ready Express APIs with first-class TypeScript or JavaScript support, centralized error handling, Zod validation, structured logging, optional JWT authentication, **auto-generated Swagger documentation**, **optional payment processing (Stripe & LemonSqueezy)**, and a revolutionary repository pattern for database abstraction.**
|
|
4
4
|
|
|
5
5
|
[](https://nodejs.org/)
|
|
6
6
|
[](https://expressjs.com/)
|
|
@@ -8,9 +8,25 @@
|
|
|
8
8
|
[](https://www.typescriptlang.org/)
|
|
9
9
|
[](LICENSE)
|
|
10
10
|
|
|
11
|
-
## What's New in v2.
|
|
11
|
+
## What's New in v2.3
|
|
12
12
|
|
|
13
|
-
###
|
|
13
|
+
### 💳 Payment Processing Module (@charcoles/payments)
|
|
14
|
+
|
|
15
|
+
Professional payment processing integrated directly into Charcole projects:
|
|
16
|
+
|
|
17
|
+
- **Stripe adapter** - Full payment intent lifecycle (create, refund, status, webhook verification)
|
|
18
|
+
- **LemonSqueezy adapter** - Perfect for Pakistani developers (Stripe doesn't support PKR payouts; LemonSqueezy provides merchant-of-record payments with full Pakistani bank payout support)
|
|
19
|
+
- **Adapter pattern** - Switch between Stripe and LemonSqueezy via `PAYMENT_PROVIDER` environment variable
|
|
20
|
+
- **Optional module** - Include/exclude during project creation with provider selection
|
|
21
|
+
- **Webhook handling** - Secure webhook event processing with signature verification
|
|
22
|
+
- **Full APIs included** - Four production-ready endpoints:
|
|
23
|
+
- `POST /api/payments/create-intent` - Create Stripe PaymentIntent or LemonSqueezy checkout session
|
|
24
|
+
- `POST /api/payments/refund` - Full or partial refunds
|
|
25
|
+
- `GET /api/payments/status/:paymentId` - Normalized payment status across providers
|
|
26
|
+
- `POST /api/payments/webhook` - Secure webhook event handling
|
|
27
|
+
- **Standalone package** - Use `@charcoles/payments` in any Express.js project
|
|
28
|
+
|
|
29
|
+
### Previous: Auto-Generated Swagger Documentation (@charcoles/swagger)
|
|
14
30
|
|
|
15
31
|
The game-changing feature that eliminates 60-80% of API documentation overhead:
|
|
16
32
|
|
|
@@ -108,6 +124,7 @@ npx create-charcole@latest
|
|
|
108
124
|
# 1. Language: TypeScript or JavaScript
|
|
109
125
|
# 2. JWT Authentication: Yes/No (includes complete auth system)
|
|
110
126
|
# 3. Swagger Documentation: Yes/No (auto-generated from Zod schemas)
|
|
127
|
+
# 4. Payment Processing: Yes/No (Stripe, LemonSqueezy, or Both)
|
|
111
128
|
|
|
112
129
|
# Configure environment
|
|
113
130
|
cp .env.example .env
|
|
@@ -173,6 +190,77 @@ setupSwagger(app, {
|
|
|
173
190
|
|
|
174
191
|
**See complete guide:** `src/lib/swagger/SWAGGER_GUIDE.md` (when swagger is enabled)
|
|
175
192
|
|
|
193
|
+
## Payment Processing: Drop-in Payments for Any Provider
|
|
194
|
+
|
|
195
|
+
### The Problem
|
|
196
|
+
|
|
197
|
+
Payment integration typically requires:
|
|
198
|
+
|
|
199
|
+
- Complex Stripe/payment provider setup
|
|
200
|
+
- Different APIs for different providers
|
|
201
|
+
- Manual webhook handling
|
|
202
|
+
- Difficult provider switching
|
|
203
|
+
|
|
204
|
+
### The Solution
|
|
205
|
+
|
|
206
|
+
Charcole's payment module abstracts payment providers:
|
|
207
|
+
|
|
208
|
+
```javascript
|
|
209
|
+
// Switch providers with one environment variable
|
|
210
|
+
PAYMENT_PROVIDER=stripe // Uses Stripe
|
|
211
|
+
PAYMENT_PROVIDER=lemonsqueezy // Uses LemonSqueezy
|
|
212
|
+
|
|
213
|
+
// Same API regardless of provider
|
|
214
|
+
POST /api/payments/create-intent
|
|
215
|
+
POST /api/payments/refund
|
|
216
|
+
GET /api/payments/status/:paymentId
|
|
217
|
+
POST /api/payments/webhook
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Why Two Providers?
|
|
221
|
+
|
|
222
|
+
**For Pakistani Developers:**
|
|
223
|
+
|
|
224
|
+
- ❌ Stripe - Does NOT support PKR payouts to Pakistani bank accounts
|
|
225
|
+
- ✅ LemonSqueezy - Merchant-of-record platform with full Pakistani bank payout support
|
|
226
|
+
|
|
227
|
+
**For Global Developers:**
|
|
228
|
+
|
|
229
|
+
- ✅ Stripe - Industry standard with global support
|
|
230
|
+
- ✅ LemonSqueezy - Alternative with regional payment solutions
|
|
231
|
+
|
|
232
|
+
### Setup
|
|
233
|
+
|
|
234
|
+
1. **During project creation:**
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
npx create-charcole@latest my-api
|
|
238
|
+
# Select payment processing: Yes → Choose Stripe/LemonSqueezy/Both
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
2. **For existing projects:**
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
npm install @charcoles/payments
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
3. **Configure environment:**
|
|
248
|
+
```env
|
|
249
|
+
PAYMENT_PROVIDER=stripe # or lemonsqueezy
|
|
250
|
+
STRIPE_SECRET_KEY=sk_...
|
|
251
|
+
STRIPE_WEBHOOK_SECRET=whsec_...
|
|
252
|
+
LEMONSQUEEZY_API_KEY=...
|
|
253
|
+
LEMONSQUEEZY_WEBHOOK_SECRET=...
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Payment routes
|
|
257
|
+
|
|
258
|
+
Payment endpoints are automatically documented with Swagger when enabled:
|
|
259
|
+
|
|
260
|
+
- Routes: `/api/payments/create-intent`, `/api/payments/refund`, `/api/payments/status/{paymentId}`, `/api/payments/webhook`
|
|
261
|
+
- All routes use route-level `@swagger` comments and Zod schemas
|
|
262
|
+
- For webhook routes, raw JSON middleware is auto-configured: `app.use('/payments/webhook', express.raw({ type: 'application/json' }))`
|
|
263
|
+
|
|
176
264
|
## Repository Pattern: A Game Changer
|
|
177
265
|
|
|
178
266
|
### The Problem
|
|
@@ -349,7 +437,6 @@ We welcome contributions! Please:
|
|
|
349
437
|
6. Update README.md for significant changes
|
|
350
438
|
7. If adding Swagger docs, use Zod schemas as single source of truth
|
|
351
439
|
|
|
352
|
-
|
|
353
440
|
Good luck!!!
|
|
354
441
|
|
|
355
442
|
## 📄 License
|
package/bin/index.js
CHANGED
|
@@ -132,13 +132,31 @@ function copyDirRecursive(src, dest, excludeFiles = [], excludeDirs = []) {
|
|
|
132
132
|
message: "Include auto-generated Swagger documentation?",
|
|
133
133
|
initial: true,
|
|
134
134
|
},
|
|
135
|
+
{
|
|
136
|
+
type: "confirm",
|
|
137
|
+
name: "includePayments",
|
|
138
|
+
message: "Include payments module? (Stripe / LemonSqueezy)",
|
|
139
|
+
initial: false,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
type: (prev, values) => (values.includePayments ? "select" : null),
|
|
143
|
+
name: "paymentProvider",
|
|
144
|
+
message: "Which payment provider will you use?",
|
|
145
|
+
choices: [
|
|
146
|
+
{ title: "Stripe (global)", value: "stripe" },
|
|
147
|
+
{ title: "LemonSqueezy (Pakistan + global)", value: "lemonsqueezy" },
|
|
148
|
+
{ title: "Both (I'll switch via env var)", value: "both" },
|
|
149
|
+
],
|
|
150
|
+
initial: 0,
|
|
151
|
+
},
|
|
135
152
|
);
|
|
136
153
|
|
|
137
154
|
const responses = await prompts(questions);
|
|
138
155
|
|
|
139
156
|
// Use command line project name if provided, otherwise use prompt response
|
|
140
157
|
const projectName = projectNameFromArgs || responses.projectName;
|
|
141
|
-
const { language, auth, swagger } =
|
|
158
|
+
const { language, auth, swagger, includePayments, paymentProvider } =
|
|
159
|
+
responses;
|
|
142
160
|
|
|
143
161
|
if (!projectName || projectName.trim() === "") {
|
|
144
162
|
console.error("❌ Project name is required");
|
|
@@ -347,6 +365,94 @@ function copyDirRecursive(src, dest, excludeFiles = [], excludeDirs = []) {
|
|
|
347
365
|
}
|
|
348
366
|
}
|
|
349
367
|
|
|
368
|
+
// Handle payments module if selected
|
|
369
|
+
if (includePayments) {
|
|
370
|
+
console.log("\n📦 Adding payments module...");
|
|
371
|
+
|
|
372
|
+
// The payments module is in src/modules/payments in the template
|
|
373
|
+
const paymentsModulePath = path.join(
|
|
374
|
+
templateDir,
|
|
375
|
+
"src",
|
|
376
|
+
"modules",
|
|
377
|
+
"payments",
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
if (!fs.existsSync(paymentsModulePath)) {
|
|
381
|
+
console.error(`❌ Payments module not found at ${paymentsModulePath}`);
|
|
382
|
+
} else {
|
|
383
|
+
// 1. Merge payments module's package.json
|
|
384
|
+
const paymentsPkgPath = path.join(paymentsModulePath, "package.json");
|
|
385
|
+
|
|
386
|
+
if (fs.existsSync(paymentsPkgPath)) {
|
|
387
|
+
try {
|
|
388
|
+
const paymentsPkg = JSON.parse(
|
|
389
|
+
fs.readFileSync(paymentsPkgPath, "utf-8"),
|
|
390
|
+
);
|
|
391
|
+
console.log("✓ Found payments module package.json");
|
|
392
|
+
|
|
393
|
+
mergedPkg = mergePackageJson(mergedPkg, paymentsPkg);
|
|
394
|
+
console.log("✓ Merged payments module dependencies");
|
|
395
|
+
console.log(
|
|
396
|
+
" Added dependencies:",
|
|
397
|
+
Object.keys(paymentsPkg.dependencies || {}).join(", "),
|
|
398
|
+
);
|
|
399
|
+
if (paymentsPkg.devDependencies) {
|
|
400
|
+
console.log(
|
|
401
|
+
" Added devDependencies:",
|
|
402
|
+
Object.keys(paymentsPkg.devDependencies).join(", "),
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
} catch (error) {
|
|
406
|
+
console.error(
|
|
407
|
+
`❌ Failed to parse payments module package.json:`,
|
|
408
|
+
error.message,
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
} else {
|
|
412
|
+
console.error(
|
|
413
|
+
"❌ Payments module package.json not found at:",
|
|
414
|
+
paymentsPkgPath,
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const targetPaymentsPath = path.join(targetModulesDir, "payments");
|
|
419
|
+
console.log(
|
|
420
|
+
`Copying payments module to: ${targetPaymentsPath} (excluding package.json)`,
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
copyDirRecursive(
|
|
424
|
+
paymentsModulePath,
|
|
425
|
+
targetPaymentsPath,
|
|
426
|
+
["package.json"],
|
|
427
|
+
[],
|
|
428
|
+
);
|
|
429
|
+
console.log(
|
|
430
|
+
"✓ Copied payments module files (package.json was excluded)",
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
const copiedPkgPath = path.join(targetPaymentsPath, "package.json");
|
|
434
|
+
if (fs.existsSync(copiedPkgPath)) {
|
|
435
|
+
console.warn(
|
|
436
|
+
"⚠️ package.json was accidentally copied, removing it...",
|
|
437
|
+
);
|
|
438
|
+
fs.unlinkSync(copiedPkgPath);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
} else {
|
|
442
|
+
console.log("\n⏭️ Skipping payments module");
|
|
443
|
+
|
|
444
|
+
const targetPaymentsPath = path.join(
|
|
445
|
+
targetDir,
|
|
446
|
+
"src",
|
|
447
|
+
"modules",
|
|
448
|
+
"payments",
|
|
449
|
+
);
|
|
450
|
+
if (fs.existsSync(targetPaymentsPath)) {
|
|
451
|
+
console.log("Cleaning up payments directory (not selected)...");
|
|
452
|
+
fs.rmSync(targetPaymentsPath, { recursive: true, force: true });
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
350
456
|
// Remove Swagger imports and setup from app file if not selected
|
|
351
457
|
if (!swagger) {
|
|
352
458
|
console.log("\n🧹 Removing Swagger references from app file...");
|
|
@@ -430,6 +536,20 @@ function copyDirRecursive(src, dest, excludeFiles = [], excludeDirs = []) {
|
|
|
430
536
|
exampleContent = `APP_NAME=CHARCOLE API\n` + exampleContent;
|
|
431
537
|
}
|
|
432
538
|
|
|
539
|
+
if (includePayments) {
|
|
540
|
+
if (paymentProvider === "both") {
|
|
541
|
+
exampleContent = exampleContent.replace(
|
|
542
|
+
"PAYMENT_PROVIDER=",
|
|
543
|
+
'PAYMENT_PROVIDER= # Set to "stripe" or "lemonsqueezy"',
|
|
544
|
+
);
|
|
545
|
+
} else {
|
|
546
|
+
exampleContent = exampleContent.replace(
|
|
547
|
+
"PAYMENT_PROVIDER=",
|
|
548
|
+
`PAYMENT_PROVIDER=${paymentProvider}`,
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
433
553
|
fs.writeFileSync(envPath, exampleContent, "utf-8");
|
|
434
554
|
console.log("✓ Created .env from .env.example with default APP_NAME");
|
|
435
555
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-charcole",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "CLI to create production-ready Node.js Express APIs with TypeScript/JavaScript, JWT auth, auto-generated Swagger docs, and repository pattern",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [1.0.0] - 2024-04-29
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Initial release
|
|
8
|
+
- StripeAdapter: createPayment, refundPayment, getPaymentStatus, verifyWebhook
|
|
9
|
+
- LemonSqueezyAdapter: same interface with regional payment support
|
|
10
|
+
- setupPayments() Express integration function
|
|
11
|
+
- Zod validation for all inputs
|
|
12
|
+
- In-memory webhook deduplication
|
|
13
|
+
- Full TypeScript definitions
|
|
14
|
+
- Comprehensive test suite with Vitest
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# @charcoles/payments
|
|
2
|
+
|
|
3
|
+
Drop-in payment processing for Express apps. Supports Stripe and LemonSqueezy for regional payments (Pakistan, etc.).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @charcoles/payments
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
### Standalone Express App
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import express from "express";
|
|
17
|
+
import { setupPayments } from "@charcoles/payments";
|
|
18
|
+
|
|
19
|
+
const app = express();
|
|
20
|
+
|
|
21
|
+
// IMPORTANT: Register webhook raw body middleware BEFORE express.json()
|
|
22
|
+
app.use("/payments/webhook", express.raw({ type: "application/json" }));
|
|
23
|
+
|
|
24
|
+
// Global JSON parsing for other routes
|
|
25
|
+
app.use(express.json());
|
|
26
|
+
|
|
27
|
+
// Setup payments
|
|
28
|
+
setupPayments(app, {
|
|
29
|
+
provider: "lemonsqueezy",
|
|
30
|
+
lemonSqueezyApiKey: process.env.LEMONSQUEEZY_API_KEY,
|
|
31
|
+
lemonSqueezyWebhookSecret: process.env.LEMONSQUEEZY_WEBHOOK_SECRET,
|
|
32
|
+
lemonSqueezyStoreId: process.env.LEMONSQUEEZY_STORE_ID,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
app.listen(3000);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Environment Variables
|
|
39
|
+
|
|
40
|
+
| Variable | Stripe | LemonSqueezy | Description |
|
|
41
|
+
| ----------------------------- | ------ | ------------ | ---------------------------------- |
|
|
42
|
+
| `PAYMENT_PROVIDER` | ✅ | ✅ | `"stripe"` or `"lemonsqueezy"` |
|
|
43
|
+
| `STRIPE_SECRET_KEY` | ✅ | ❌ | `sk_live_...` or `sk_test_...` |
|
|
44
|
+
| `STRIPE_WEBHOOK_SECRET` | ✅ | ❌ | `whsec_...` from Stripe dashboard |
|
|
45
|
+
| `LEMONSQUEEZY_API_KEY` | ❌ | ✅ | From LS API settings |
|
|
46
|
+
| `LEMONSQUEEZY_WEBHOOK_SECRET` | ❌ | ✅ | From LS webhook settings |
|
|
47
|
+
| `LEMONSQUEEZY_STORE_ID` | ❌ | ✅ | Numeric store ID from LS dashboard |
|
|
48
|
+
|
|
49
|
+
### Stripe Configuration
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
setupPayments(app, {
|
|
53
|
+
provider: "stripe",
|
|
54
|
+
stripeSecretKey: "sk_live_...",
|
|
55
|
+
stripeWebhookSecret: "whsec_...",
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### LemonSqueezy Configuration
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
setupPayments(app, {
|
|
63
|
+
provider: "lemonsqueezy",
|
|
64
|
+
lemonSqueezyApiKey: "your_api_key",
|
|
65
|
+
lemonSqueezyWebhookSecret: "your_webhook_secret",
|
|
66
|
+
lemonSqueezyStoreId: "12345",
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## API Endpoints
|
|
71
|
+
|
|
72
|
+
### POST /payments/create-intent
|
|
73
|
+
|
|
74
|
+
Create a payment intent or checkout session.
|
|
75
|
+
|
|
76
|
+
**Request:**
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"amount": 2999,
|
|
81
|
+
"currency": "usd",
|
|
82
|
+
"metadata": {
|
|
83
|
+
"orderId": "order_123"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Response (Stripe):**
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"success": true,
|
|
93
|
+
"data": {
|
|
94
|
+
"id": "pi_3abc...",
|
|
95
|
+
"clientSecret": "pi_..._secret_...",
|
|
96
|
+
"status": "requires_payment_method",
|
|
97
|
+
"amount": 2999,
|
|
98
|
+
"currency": "usd"
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Response (LemonSqueezy):**
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"success": true,
|
|
108
|
+
"data": {
|
|
109
|
+
"id": "12345",
|
|
110
|
+
"checkoutUrl": "https://app.lemonsqueezy.com/checkout/...",
|
|
111
|
+
"status": "created",
|
|
112
|
+
"amount": 2999,
|
|
113
|
+
"currency": "usd"
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### POST /payments/refund
|
|
119
|
+
|
|
120
|
+
Refund a payment.
|
|
121
|
+
|
|
122
|
+
**Request:**
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"paymentId": "pi_3abc...",
|
|
127
|
+
"amount": 1500
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### GET /payments/status/:paymentId
|
|
132
|
+
|
|
133
|
+
Get payment status.
|
|
134
|
+
|
|
135
|
+
**Response:**
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"success": true,
|
|
140
|
+
"data": {
|
|
141
|
+
"id": "pi_3abc...",
|
|
142
|
+
"status": "paid",
|
|
143
|
+
"amount": 2999,
|
|
144
|
+
"currency": "usd"
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### POST /payments/webhook
|
|
150
|
+
|
|
151
|
+
Receive provider webhooks. Returns `{"received": true}`.
|
|
152
|
+
|
|
153
|
+
## LemonSqueezy Notes
|
|
154
|
+
|
|
155
|
+
### Regional Support
|
|
156
|
+
|
|
157
|
+
LemonSqueezy is included specifically for developers in regions where Stripe payouts are not available (e.g., Pakistan). LemonSqueezy uses a merchant-of-record model, so you sell through their entity and receive bank transfers.
|
|
158
|
+
|
|
159
|
+
### Variant IDs Required
|
|
160
|
+
|
|
161
|
+
Unlike Stripe (which accepts raw amounts), LemonSqueezy requires a Product Variant ID. Create a "Pay What You Want" product in your LemonSqueezy dashboard and pass the variant ID in `metadata.variantId`:
|
|
162
|
+
|
|
163
|
+
```json
|
|
164
|
+
{
|
|
165
|
+
"amount": 10000,
|
|
166
|
+
"currency": "pkr",
|
|
167
|
+
"metadata": {
|
|
168
|
+
"variantId": "12345"
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Raw Body Middleware Warning
|
|
174
|
+
|
|
175
|
+
⚠️ **Critical**: Webhook endpoints require raw `Buffer` bodies for signature verification. Always register `express.raw({ type: 'application/json' })` on the webhook path **before** `express.json()`:
|
|
176
|
+
|
|
177
|
+
```js
|
|
178
|
+
// ✅ Correct order
|
|
179
|
+
app.use("/payments/webhook", express.raw({ type: "application/json" }));
|
|
180
|
+
app.use(express.json()); // Global JSON parsing
|
|
181
|
+
|
|
182
|
+
// ❌ Wrong order — webhooks will fail
|
|
183
|
+
app.use(express.json());
|
|
184
|
+
app.use("/payments/webhook", express.raw({ type: "application/json" }));
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## TypeScript Support
|
|
188
|
+
|
|
189
|
+
Full TypeScript definitions included:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
import {
|
|
193
|
+
setupPayments,
|
|
194
|
+
PaymentAdapter,
|
|
195
|
+
StripeAdapter,
|
|
196
|
+
} from "@charcoles/payments";
|
|
197
|
+
|
|
198
|
+
interface MyOptions {
|
|
199
|
+
provider: "stripe";
|
|
200
|
+
stripeSecretKey: string;
|
|
201
|
+
stripeWebhookSecret: string;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
setupPayments(app, options);
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Error Handling
|
|
208
|
+
|
|
209
|
+
All errors are `PaymentError` instances with specific codes:
|
|
210
|
+
|
|
211
|
+
- `CONFIG_ERROR`: Missing required configuration
|
|
212
|
+
- `WEBHOOK_INVALID`: Invalid webhook signature
|
|
213
|
+
- `STRIPE_ERROR`: Stripe API error
|
|
214
|
+
- `LS_CHECKOUT_FAILED`: LemonSqueezy checkout creation failed
|
|
215
|
+
- `LS_REFUND_FAILED`: LemonSqueezy refund failed
|
|
216
|
+
- `LS_ORDER_NOT_FOUND`: LemonSqueezy order not found
|
|
217
|
+
- `MISSING_VARIANT_ID`: LemonSqueezy variantId not provided
|
|
218
|
+
- `PROVIDER_NOT_CONFIGURED`: PAYMENT_PROVIDER env var missing
|
|
219
|
+
|
|
220
|
+
## License
|
|
221
|
+
|
|
222
|
+
ISC
|
|
Binary file
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@charcoles/payments",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Drop-in payment processing for Express apps. Stripe + LemonSqueezy.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"types": "./src/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./src/index.js",
|
|
11
|
+
"types": "./src/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"author": {
|
|
15
|
+
"name": "Sheraz Manzoor",
|
|
16
|
+
"email": "sheraz.dev121@gmail.com"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/sheraz4196/charcole.git",
|
|
21
|
+
"directory": "packages/payments"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/sheraz4196/charcole/issues"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://www.charcole.site/guides/payments/introduction",
|
|
27
|
+
"files": [
|
|
28
|
+
"src",
|
|
29
|
+
"README.md",
|
|
30
|
+
"CHANGELOG.md"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "node build.js",
|
|
34
|
+
"test": "vitest",
|
|
35
|
+
"test:run": "vitest run"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"express": "^4.18.0 || ^5.0.0"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"stripe": "^14.0.0",
|
|
42
|
+
"@lemonsqueezy/lemonsqueezy.js": "^4.0.0",
|
|
43
|
+
"zod": "^3.22.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/express": "^4.17.0",
|
|
47
|
+
"@types/node": "^20.0.0",
|
|
48
|
+
"vitest": "^1.0.0"
|
|
49
|
+
},
|
|
50
|
+
"keywords": [
|
|
51
|
+
"payments",
|
|
52
|
+
"stripe",
|
|
53
|
+
"lemonsqueezy",
|
|
54
|
+
"express",
|
|
55
|
+
"charcole"
|
|
56
|
+
],
|
|
57
|
+
"license": "ISC",
|
|
58
|
+
"publishConfig": {
|
|
59
|
+
"access": "public"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { PaymentError } from "./src/errors/PaymentError.js";
|
|
2
|
+
import { StripeAdapter } from "./src/adapters/StripeAdapter.js";
|
|
3
|
+
|
|
4
|
+
// Test 1: PaymentError works
|
|
5
|
+
const err = new PaymentError("test", "TEST_CODE", 400);
|
|
6
|
+
console.assert(err.name === "PaymentError", "FAIL: PaymentError.name");
|
|
7
|
+
console.assert(err.code === "TEST_CODE", "FAIL: PaymentError.code");
|
|
8
|
+
console.assert(err.statusCode === 400, "FAIL: PaymentError.statusCode");
|
|
9
|
+
console.log("✅ PaymentError works");
|
|
10
|
+
|
|
11
|
+
// Test 2: StripeAdapter rejects missing config
|
|
12
|
+
try {
|
|
13
|
+
new StripeAdapter({});
|
|
14
|
+
console.log("❌ StripeAdapter should have thrown");
|
|
15
|
+
} catch (e) {
|
|
16
|
+
console.assert(e.code === "CONFIG_ERROR", "FAIL: wrong error code");
|
|
17
|
+
console.log("✅ StripeAdapter rejects missing config");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
console.log("\nSmoke test complete.");
|