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.
Files changed (64) hide show
  1. package/CHANGELOG.md +33 -2
  2. package/README.md +92 -5
  3. package/bin/index.js +121 -1
  4. package/package.json +1 -1
  5. package/packages/payments/CHANGELOG.md +14 -0
  6. package/packages/payments/README.md +222 -0
  7. package/packages/payments/charcoles-payments-1.0.0.tgz +0 -0
  8. package/packages/payments/package.json +61 -0
  9. package/packages/payments/smoke-test.js +20 -0
  10. package/packages/payments/src/__tests__/LemonSqueezyAdapter.test.js +236 -0
  11. package/packages/payments/src/__tests__/StripeAdapter.test.js +139 -0
  12. package/packages/payments/src/__tests__/payments.service.test.js +131 -0
  13. package/packages/payments/src/__tests__/webhookUtils.test.js +47 -0
  14. package/packages/payments/src/adapters/LemonSqueezyAdapter.js +150 -0
  15. package/packages/payments/src/adapters/PaymentAdapter.js +109 -0
  16. package/packages/payments/src/adapters/StripeAdapter.js +114 -0
  17. package/packages/payments/src/controllers/payments.controller.js +48 -0
  18. package/packages/payments/src/errors/PaymentError.js +8 -0
  19. package/packages/payments/src/helpers/webhookUtils.js +27 -0
  20. package/packages/payments/src/index.d.ts +87 -0
  21. package/packages/payments/src/index.js +6 -0
  22. package/packages/payments/src/routes/payments.routes.js +41 -0
  23. package/packages/payments/src/schemas/payments.schemas.js +24 -0
  24. package/packages/payments/src/services/payments.service.js +68 -0
  25. package/plan-2.3.0.md +1756 -0
  26. package/template/js/.env.example +19 -1
  27. package/template/js/README.md +133 -5
  28. package/template/js/basePackage.json +1 -1
  29. package/template/js/src/app.js +17 -0
  30. package/template/js/src/config/env.js +8 -0
  31. package/template/js/src/lib/swagger/SWAGGER_GUIDE.md +34 -0
  32. package/template/js/src/modules/payments/__tests__/payments.controller.test.js +342 -0
  33. package/template/js/src/modules/payments/__tests__/payments.routes.test.js +256 -0
  34. package/template/js/src/modules/payments/__tests__/payments.schemas.test.js +94 -0
  35. package/template/js/src/modules/payments/__tests__/payments.service.test.js +141 -0
  36. package/template/js/src/modules/payments/package.json +7 -0
  37. package/template/js/src/modules/payments/payments.adapter.js +47 -0
  38. package/template/js/src/modules/payments/payments.constants.js +20 -0
  39. package/template/js/src/modules/payments/payments.controller.js +85 -0
  40. package/template/js/src/modules/payments/payments.routes.js +125 -0
  41. package/template/js/src/modules/payments/payments.schemas.js +28 -0
  42. package/template/js/src/modules/payments/payments.service.js +34 -0
  43. package/template/js/src/modules/swagger/package.json +1 -1
  44. package/template/js/src/routes/index.js +16 -0
  45. package/template/ts/.env.example +17 -1
  46. package/template/ts/README.md +135 -5
  47. package/template/ts/basePackage.json +1 -1
  48. package/template/ts/src/app.ts +13 -0
  49. package/template/ts/src/config/env.ts +7 -0
  50. package/template/ts/src/lib/swagger/SWAGGER_GUIDE.md +34 -0
  51. package/template/ts/src/modules/payments/__tests__/payments.controller.test.ts +282 -0
  52. package/template/ts/src/modules/payments/__tests__/payments.routes.test.ts +256 -0
  53. package/template/ts/src/modules/payments/__tests__/payments.schemas.test.ts +94 -0
  54. package/template/ts/src/modules/payments/__tests__/payments.service.test.ts +135 -0
  55. package/template/ts/src/modules/payments/package.json +7 -0
  56. package/template/ts/src/modules/payments/payments.adapter.ts +74 -0
  57. package/template/ts/src/modules/payments/payments.constants.ts +18 -0
  58. package/template/ts/src/modules/payments/payments.controller.ts +104 -0
  59. package/template/ts/src/modules/payments/payments.routes.ts +125 -0
  60. package/template/ts/src/modules/payments/payments.schemas.ts +31 -0
  61. package/template/ts/src/modules/payments/payments.service.ts +51 -0
  62. package/template/ts/src/modules/payments/payments.types.ts +56 -0
  63. package/template/ts/src/modules/swagger/package.json +1 -1
  64. package/template/ts/src/routes/index.ts +8 -0
@@ -0,0 +1,135 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+
3
+ vi.mock('../payments.adapter.ts', () => ({
4
+ getAdapter: vi.fn(),
5
+ resetAdapter: vi.fn(),
6
+ }))
7
+
8
+ import { getAdapter } from '../payments.adapter.ts'
9
+ import * as service from '../payments.service.ts'
10
+
11
+ beforeEach(() => {
12
+ vi.clearAllMocks()
13
+ })
14
+
15
+ describe('payments.service', () => {
16
+ it('delegates to adapter.createPayment and returns its result', async () => {
17
+ const mockResult = {
18
+ id: 'pi_test_123',
19
+ clientSecret: 'secret_abc',
20
+ status: 'requires_payment_method',
21
+ amount: 999,
22
+ currency: 'usd',
23
+ metadata: {},
24
+ }
25
+ const adapter = { createPayment: vi.fn().mockResolvedValue(mockResult) }
26
+ getAdapter.mockReturnValue(adapter)
27
+
28
+ const result = await service.createPayment({ amount: 999, currency: 'usd' })
29
+
30
+ expect(adapter.createPayment).toHaveBeenCalledWith({ amount: 999, currency: 'usd' })
31
+ expect(result).toEqual(mockResult)
32
+ })
33
+
34
+ it('propagates errors from adapter.createPayment', async () => {
35
+ const error = new Error('payment failed')
36
+ const adapter = { createPayment: vi.fn().mockRejectedValue(error) }
37
+ getAdapter.mockReturnValue(adapter)
38
+
39
+ await expect(service.createPayment({ amount: 999, currency: 'usd' })).rejects.toThrow(error)
40
+ })
41
+
42
+ it('delegates to adapter.refundPayment with paymentId and amount', async () => {
43
+ const mockResult = { id: 're_test_123', status: 'succeeded', amount: 500 }
44
+ const adapter = { refundPayment: vi.fn().mockResolvedValue(mockResult) }
45
+ getAdapter.mockReturnValue(adapter)
46
+
47
+ const result = await service.refundPayment({ paymentId: 'pi_123', amount: 500 })
48
+
49
+ expect(adapter.refundPayment).toHaveBeenCalledWith({ paymentId: 'pi_123', amount: 500 })
50
+ expect(result).toEqual(mockResult)
51
+ })
52
+
53
+ it('delegates to adapter.refundPayment without amount when not provided', async () => {
54
+ const mockResult = { id: 're_test_124', status: 'succeeded', amount: 0 }
55
+ const adapter = { refundPayment: vi.fn().mockResolvedValue(mockResult) }
56
+ getAdapter.mockReturnValue(adapter)
57
+
58
+ const result = await service.refundPayment({ paymentId: 'pi_123' })
59
+
60
+ expect(adapter.refundPayment).toHaveBeenCalledWith({ paymentId: 'pi_123', amount: undefined })
61
+ expect(result).toEqual(mockResult)
62
+ })
63
+
64
+ it('propagates adapter errors from refundPayment', async () => {
65
+ const error = new Error('refund failed')
66
+ const adapter = { refundPayment: vi.fn().mockRejectedValue(error) }
67
+ getAdapter.mockReturnValue(adapter)
68
+
69
+ await expect(service.refundPayment({ paymentId: 'pi_123', amount: 500 })).rejects.toThrow(error)
70
+ })
71
+
72
+ it('delegates to adapter.getPaymentStatus with the paymentId string', async () => {
73
+ const mockResult = { id: 'pi_123', status: 'paid', amount: 999, currency: 'usd', metadata: {} }
74
+ const adapter = { getPaymentStatus: vi.fn().mockResolvedValue(mockResult) }
75
+ getAdapter.mockReturnValue(adapter)
76
+
77
+ const result = await service.getPaymentStatus('pi_123')
78
+
79
+ expect(adapter.getPaymentStatus).toHaveBeenCalledWith('pi_123')
80
+ expect(result).toEqual(mockResult)
81
+ })
82
+
83
+ it('propagates adapter errors from getPaymentStatus', async () => {
84
+ const error = new Error('status error')
85
+ const adapter = { getPaymentStatus: vi.fn().mockRejectedValue(error) }
86
+ getAdapter.mockReturnValue(adapter)
87
+
88
+ await expect(service.getPaymentStatus('pi_123')).rejects.toThrow(error)
89
+ })
90
+
91
+ it('calls adapter.verifyWebhook and returns duplicate:false on first call', async () => {
92
+ const adapter = { verifyWebhook: vi.fn().mockResolvedValue({ event: 'payment_intent.succeeded', data: { id: 'evt_001' } }) }
93
+ getAdapter.mockReturnValue(adapter)
94
+
95
+ const result = await service.processWebhook(Buffer.from('{"id":"evt_001"}'), 'sig')
96
+
97
+ expect(adapter.verifyWebhook).toHaveBeenCalledWith(Buffer.from('{"id":"evt_001"}'), 'sig')
98
+ expect(result.duplicate).toBe(false)
99
+ expect(result.event).toBe('payment_intent.succeeded')
100
+ })
101
+
102
+ it('returns duplicate:true on second call with same event id', async () => {
103
+ const event = { event: 'payment_intent.succeeded', data: { id: 'evt_002' } }
104
+ const adapter = { verifyWebhook: vi.fn().mockResolvedValue(event) }
105
+ getAdapter.mockReturnValue(adapter)
106
+
107
+ const first = await service.processWebhook(Buffer.from('{"id":"evt_002"}'), 'sig')
108
+ const second = await service.processWebhook(Buffer.from('{"id":"evt_002"}'), 'sig')
109
+
110
+ expect(first.duplicate).toBe(false)
111
+ expect(second.duplicate).toBe(true)
112
+ expect(adapter.verifyWebhook).toHaveBeenCalledTimes(2)
113
+ })
114
+
115
+ it('treats events without id as unique using event+timestamp', async () => {
116
+ const adapter = { verifyWebhook: vi.fn().mockResolvedValue({ event: 'some_event', data: {} }) }
117
+ getAdapter.mockReturnValue(adapter)
118
+
119
+ const first = await service.processWebhook(Buffer.from('{"id":"evt_none_1"}'), 'sig')
120
+ await new Promise((resolve) => setTimeout(resolve, 1))
121
+ const second = await service.processWebhook(Buffer.from('{"id":"evt_none_2"}'), 'sig')
122
+
123
+ expect(first.duplicate).toBe(false)
124
+ expect(second.duplicate).toBe(false)
125
+ expect(adapter.verifyWebhook).toHaveBeenCalledTimes(2)
126
+ })
127
+
128
+ it('propagates PaymentError WEBHOOK_INVALID from adapter', async () => {
129
+ const error = new Error('bad sig')
130
+ const adapter = { verifyWebhook: vi.fn().mockRejectedValue(error) }
131
+ getAdapter.mockReturnValue(adapter)
132
+
133
+ await expect(service.processWebhook(Buffer.from('{"id":"evt_003"}'), 'sig')).rejects.toThrow(error)
134
+ })
135
+ })
@@ -0,0 +1,7 @@
1
+ {
2
+ "dependencies": {
3
+ "@charcoles/payments": "^1.0.1",
4
+ "stripe": "^14.0.0",
5
+ "@lemonsqueezy/lemonsqueezy.js": "^4.0.0"
6
+ }
7
+ }
@@ -0,0 +1,74 @@
1
+ import { env } from "../../config/env.ts";
2
+ import {
3
+ StripeAdapter,
4
+ LemonSqueezyAdapter,
5
+ PaymentError,
6
+ } from "@charcoles/payments";
7
+ import type { PaymentAdapter } from "./payments.types.ts";
8
+
9
+ let adapter: PaymentAdapter | null = null;
10
+
11
+ export function getAdapter(): PaymentAdapter {
12
+ if (adapter) {
13
+ return adapter;
14
+ }
15
+
16
+ const provider = env.PAYMENT_PROVIDER;
17
+
18
+ if (!provider) {
19
+ const error = new PaymentError(
20
+ 'PAYMENT_PROVIDER env var is not set. Set it to "stripe" or "lemonsqueezy".',
21
+ );
22
+ error.code = "PROVIDER_NOT_CONFIGURED";
23
+ error.statusCode = 500;
24
+ throw error;
25
+ }
26
+
27
+ if (provider === "stripe") {
28
+ if (!env.STRIPE_SECRET_KEY || !env.STRIPE_WEBHOOK_SECRET) {
29
+ const error = new PaymentError(
30
+ "Stripe configuration is incomplete. Set STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET.",
31
+ );
32
+ error.code = "CONFIG_ERROR";
33
+ error.statusCode = 500;
34
+ throw error;
35
+ }
36
+
37
+ adapter = new StripeAdapter({
38
+ secretKey: env.STRIPE_SECRET_KEY,
39
+ webhookSecret: env.STRIPE_WEBHOOK_SECRET,
40
+ }) as unknown as PaymentAdapter;
41
+ } else if (provider === "lemonsqueezy") {
42
+ if (
43
+ !env.LEMONSQUEEZY_API_KEY ||
44
+ !env.LEMONSQUEEZY_WEBHOOK_SECRET ||
45
+ !env.LEMONSQUEEZY_STORE_ID
46
+ ) {
47
+ const error = new PaymentError(
48
+ "LemonSqueezy configuration is incomplete. Set LEMONSQUEEZY_API_KEY, LEMONSQUEEZY_WEBHOOK_SECRET, and LEMONSQUEEZY_STORE_ID.",
49
+ );
50
+ error.code = "CONFIG_ERROR";
51
+ error.statusCode = 500;
52
+ throw error;
53
+ }
54
+
55
+ adapter = new LemonSqueezyAdapter({
56
+ apiKey: env.LEMONSQUEEZY_API_KEY,
57
+ webhookSecret: env.LEMONSQUEEZY_WEBHOOK_SECRET,
58
+ storeId: env.LEMONSQUEEZY_STORE_ID,
59
+ }) as unknown as PaymentAdapter;
60
+ } else {
61
+ const error = new PaymentError(
62
+ `Unknown PAYMENT_PROVIDER: "${provider}". Use "stripe" or "lemonsqueezy".`,
63
+ );
64
+ error.code = "CONFIG_ERROR";
65
+ error.statusCode = 500;
66
+ throw error;
67
+ }
68
+
69
+ return adapter;
70
+ }
71
+
72
+ export function resetAdapter(): void {
73
+ adapter = null;
74
+ }
@@ -0,0 +1,18 @@
1
+ export const PAYMENT_PROVIDERS = {
2
+ STRIPE: "stripe",
3
+ LEMONSQUEEZY: "lemonsqueezy",
4
+ } as const;
5
+
6
+ export const PAYMENT_EVENTS = {
7
+ STRIPE_PAYMENT_SUCCEEDED: "payment_intent.succeeded",
8
+ STRIPE_PAYMENT_FAILED: "payment_intent.payment_failed",
9
+ STRIPE_REFUND_CREATED: "charge.refunded",
10
+ LS_ORDER_CREATED: "order_created",
11
+ LS_ORDER_REFUNDED: "order_refunded",
12
+ LS_SUBSCRIPTION_CANCELLED: "subscription_cancelled",
13
+ } as const;
14
+
15
+ export const WEBHOOK_HEADERS = {
16
+ stripe: "stripe-signature",
17
+ lemonsqueezy: "x-signature",
18
+ } as const;
@@ -0,0 +1,104 @@
1
+ import { Request, Response, NextFunction } from "express";
2
+ import * as paymentsService from "./payments.service.ts";
3
+ import { sendSuccess } from "../../utils/response.ts";
4
+ import {
5
+ createPaymentSchema,
6
+ refundPaymentSchema,
7
+ } from "./payments.schemas.ts";
8
+ import { PAYMENT_EVENTS, WEBHOOK_HEADERS } from "./payments.constants.ts";
9
+ import { logger } from "../../utils/logger.ts";
10
+
11
+ export const createPayment = async (
12
+ req: Request,
13
+ res: Response,
14
+ next: NextFunction,
15
+ ): Promise<void> => {
16
+ try {
17
+ const validated = createPaymentSchema.parse(req.body);
18
+ const result = await paymentsService.createPayment(validated);
19
+ sendSuccess(res, result, 201);
20
+ } catch (err) {
21
+ next(err);
22
+ }
23
+ };
24
+
25
+ export const refundPayment = async (
26
+ req: Request,
27
+ res: Response,
28
+ next: NextFunction,
29
+ ): Promise<void> => {
30
+ try {
31
+ const validated = refundPaymentSchema.parse(req.body);
32
+ const result = await paymentsService.refundPayment(validated);
33
+ sendSuccess(res, result);
34
+ } catch (err) {
35
+ next(err);
36
+ }
37
+ };
38
+
39
+ export const getPaymentStatus = async (
40
+ req: Request,
41
+ res: Response,
42
+ next: NextFunction,
43
+ ): Promise<void> => {
44
+ try {
45
+ const result = await paymentsService.getPaymentStatus(req.params.paymentId);
46
+ sendSuccess(res, result);
47
+ } catch (err) {
48
+ next(err);
49
+ }
50
+ };
51
+
52
+ export const handleWebhook = async (
53
+ req: Request,
54
+ res: Response,
55
+ next: NextFunction,
56
+ ): Promise<void> => {
57
+ try {
58
+ const provider = process.env.PAYMENT_PROVIDER;
59
+ const headerName =
60
+ provider === "stripe"
61
+ ? WEBHOOK_HEADERS.stripe
62
+ : WEBHOOK_HEADERS.lemonsqueezy;
63
+
64
+ const signature = req.headers[headerName] as string | undefined;
65
+
66
+ if (!signature) {
67
+ res.status(400).json({ error: "Missing webhook signature header" });
68
+ return;
69
+ }
70
+
71
+ const rawBody = req.body as Buffer;
72
+ const result = await paymentsService.processWebhook(rawBody, signature);
73
+
74
+ if (result.duplicate) {
75
+ logger.info(
76
+ `Duplicate webhook event received and ignored: ${result.event}`,
77
+ );
78
+ res.status(200).json({ received: true, duplicate: true });
79
+ return;
80
+ }
81
+
82
+ logger.info(`Webhook event received: ${result.event}`);
83
+
84
+ switch (result.event) {
85
+ case PAYMENT_EVENTS.STRIPE_PAYMENT_SUCCEEDED:
86
+ case PAYMENT_EVENTS.LS_ORDER_CREATED:
87
+ logger.info("Payment confirmed. Add your fulfillment logic here.");
88
+ break;
89
+ case PAYMENT_EVENTS.STRIPE_PAYMENT_FAILED:
90
+ logger.warn("Payment failed. Add your failure handling logic here.");
91
+ break;
92
+ case PAYMENT_EVENTS.LS_ORDER_REFUNDED:
93
+ case PAYMENT_EVENTS.STRIPE_REFUND_CREATED:
94
+ logger.info("Refund processed. Add your refund handling logic here.");
95
+ break;
96
+ default:
97
+ logger.info(`Unhandled webhook event: ${result.event}`);
98
+ }
99
+
100
+ res.status(200).json({ received: true });
101
+ } catch (err) {
102
+ next(err);
103
+ }
104
+ };
@@ -0,0 +1,125 @@
1
+ import { Router } from "express";
2
+ import { validateRequest } from "../../middlewares/validateRequest.ts";
3
+ import * as controller from "./payments.controller.ts";
4
+ import {
5
+ createPaymentSchema,
6
+ refundPaymentSchema,
7
+ } from "./payments.schemas.ts";
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,31 @@
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
+ });
29
+
30
+ export type CreatePaymentInput = z.infer<typeof createPaymentSchema>;
31
+ export type RefundPaymentInput = z.infer<typeof refundPaymentSchema>;
@@ -0,0 +1,51 @@
1
+ import { getAdapter } from "./payments.adapter.ts";
2
+ import type {
3
+ CreatePaymentParams,
4
+ CreatePaymentResult,
5
+ RefundParams,
6
+ RefundResult,
7
+ PaymentStatus,
8
+ WebhookResult,
9
+ } from "./payments.types.ts";
10
+
11
+ const processedWebhookIds = new Set<string>();
12
+
13
+ export async function createPayment(
14
+ params: CreatePaymentParams,
15
+ ): Promise<CreatePaymentResult> {
16
+ const adapter = getAdapter();
17
+ return adapter.createPayment(params);
18
+ }
19
+
20
+ export async function refundPayment(
21
+ params: RefundParams,
22
+ ): Promise<RefundResult> {
23
+ const adapter = getAdapter();
24
+ return adapter.refundPayment(params);
25
+ }
26
+
27
+ export async function getPaymentStatus(
28
+ paymentId: string,
29
+ ): Promise<PaymentStatus> {
30
+ const adapter = getAdapter();
31
+ return adapter.getPaymentStatus(paymentId);
32
+ }
33
+
34
+ export async function processWebhook(
35
+ rawBody: Buffer,
36
+ signature: string,
37
+ ): Promise<WebhookResult & { duplicate: boolean }> {
38
+ const adapter = getAdapter();
39
+ const result = await adapter.verifyWebhook(rawBody, signature);
40
+
41
+ const eventId = result.data?.id
42
+ ? String(result.data.id)
43
+ : `${result.event}-${Date.now()}`;
44
+
45
+ if (processedWebhookIds.has(eventId)) {
46
+ return { ...result, duplicate: true };
47
+ }
48
+
49
+ processedWebhookIds.add(eventId);
50
+ return { ...result, duplicate: false };
51
+ }
@@ -0,0 +1,56 @@
1
+ export interface CreatePaymentParams {
2
+ amount: number;
3
+ currency: string;
4
+ metadata?: Record<string, string>;
5
+ }
6
+
7
+ export interface CreatePaymentResult {
8
+ id: string;
9
+ clientSecret?: string;
10
+ checkoutUrl?: string;
11
+ status: "pending" | "requires_payment_method" | "created";
12
+ amount: number;
13
+ currency: string;
14
+ metadata: Record<string, unknown>;
15
+ }
16
+
17
+ export interface RefundParams {
18
+ paymentId: string;
19
+ amount?: number;
20
+ }
21
+
22
+ export interface RefundResult {
23
+ id: string;
24
+ status: "succeeded" | "pending" | "failed";
25
+ amount: number;
26
+ }
27
+
28
+ export interface PaymentStatus {
29
+ id: string;
30
+ status: "pending" | "paid" | "failed" | "refunded";
31
+ amount: number;
32
+ currency: string;
33
+ metadata: Record<string, unknown>;
34
+ }
35
+
36
+ export interface WebhookResult {
37
+ event: string;
38
+ data: Record<string, unknown>;
39
+ }
40
+
41
+ export interface PaymentAdapter {
42
+ createPayment(params: CreatePaymentParams): Promise<CreatePaymentResult>;
43
+ refundPayment(params: RefundParams): Promise<RefundResult>;
44
+ getPaymentStatus(paymentId: string): Promise<PaymentStatus>;
45
+ verifyWebhook(rawBody: Buffer, signature: string): Promise<WebhookResult>;
46
+ }
47
+
48
+ export interface SetupPaymentsOptions {
49
+ provider: "stripe" | "lemonsqueezy";
50
+ stripeSecretKey?: string;
51
+ stripeWebhookSecret?: string;
52
+ lemonSqueezyApiKey?: string;
53
+ lemonSqueezyWebhookSecret?: string;
54
+ lemonSqueezyStoreId?: string;
55
+ mountPath?: string;
56
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "dependencies": {
3
- "@charcoles/swagger": "file:./charcole-swagger-1.0.1.tgz"
3
+ "@charcoles/swagger": "^1.0.1"
4
4
  }
5
5
  }
@@ -1,4 +1,6 @@
1
1
  import { Router } from "express";
2
+ import { dirname } from "path";
3
+ import { fileURLToPath } from "url";
2
4
  import {
3
5
  getHealth,
4
6
  createItem,
@@ -7,6 +9,9 @@ import {
7
9
  import { validateRequest } from "../middlewares/validateRequest.ts";
8
10
  import protectedRoutes from "./protected.ts";
9
11
  import authRoutes from "../modules/auth/auth.routes.ts";
12
+ import paymentsRoutes from "../modules/payments/payments.routes.ts";
13
+
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
15
  const router = Router();
11
16
 
12
17
  // Health check
@@ -18,6 +23,9 @@ router.post("/items", validateRequest(createItemSchema), createItem);
18
23
  // 🔐 Auth routes
19
24
  router.use("/auth", authRoutes);
20
25
 
26
+ // 💳 Payment routes
27
+ router.use("/payments", paymentsRoutes);
28
+
21
29
  // 🔐 Protected routes (REQUIRED BEARER TOKEN FOR THEM)
22
30
  router.use("/protected", protectedRoutes);
23
31