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,109 @@
1
+ /**
2
+ * @typedef {Object} CreatePaymentResult
3
+ * @property {string} id - Provider-specific payment/checkout ID
4
+ * @property {string} [clientSecret] - Stripe: client_secret for frontend
5
+ * @property {string} [checkoutUrl] - LemonSqueezy: redirect URL
6
+ * @property {string} status - 'pending' | 'requires_payment_method' | 'created'
7
+ * @property {number} amount - Amount in smallest currency unit (cents)
8
+ * @property {string} currency - ISO 4217 (e.g. 'usd', 'pkr')
9
+ * @property {Object} metadata - Provider-specific raw response
10
+ */
11
+
12
+ /**
13
+ * @typedef {Object} RefundResult
14
+ * @property {string} id - Refund ID
15
+ * @property {string} status - 'succeeded' | 'pending' | 'failed'
16
+ * @property {number} amount - Refunded amount in smallest unit
17
+ */
18
+
19
+ /**
20
+ * @typedef {Object} PaymentStatus
21
+ * @property {string} id
22
+ * @property {string} status - 'pending' | 'paid' | 'failed' | 'refunded'
23
+ * @property {number} amount
24
+ * @property {string} currency
25
+ * @property {Object} metadata
26
+ */
27
+
28
+ /**
29
+ * @typedef {Object} WebhookResult
30
+ * @property {string} event
31
+ * @property {Object} data
32
+ */
33
+
34
+ /**
35
+ * Abstract PaymentAdapter interface.
36
+ * All adapters must implement these methods.
37
+ */
38
+ export class PaymentAdapter {
39
+ /**
40
+ * Create a payment intent (Stripe) or checkout session (LemonSqueezy).
41
+ * @param {Object} params
42
+ * @param {number} params.amount - Amount in smallest currency unit
43
+ * @param {string} params.currency - ISO 4217 currency code
44
+ * @param {Object} [params.metadata] - Additional metadata
45
+ * @returns {Promise<CreatePaymentResult>}
46
+ */
47
+ async createPayment(params) {
48
+ throw new Error("createPayment() must be implemented");
49
+ }
50
+
51
+ /**
52
+ * Refund a payment.
53
+ * @param {Object} params
54
+ * @param {string} params.paymentId - Payment ID to refund
55
+ * @param {number} [params.amount] - Amount to refund (omit for full refund)
56
+ * @returns {Promise<RefundResult>}
57
+ */
58
+ async refundPayment(params) {
59
+ throw new Error("refundPayment() must be implemented");
60
+ }
61
+
62
+ /**
63
+ * Get current payment status.
64
+ * @param {string} paymentId
65
+ * @returns {Promise<PaymentStatus>}
66
+ */
67
+ async getPaymentStatus(paymentId) {
68
+ throw new Error("getPaymentStatus() must be implemented");
69
+ }
70
+
71
+ /**
72
+ * Verify and parse a webhook payload.
73
+ * @param {Buffer} rawBody - Raw webhook body as Buffer
74
+ * @param {string} signature - Webhook signature from header
75
+ * @returns {Promise<WebhookResult>}
76
+ */
77
+ async verifyWebhook(rawBody, signature) {
78
+ throw new Error("verifyWebhook() must be implemented");
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Factory function to create an adapter instance.
84
+ * @param {Object} options
85
+ * @param {'stripe' | 'lemonsqueezy'} options.provider
86
+ * @param {string} [options.stripeSecretKey]
87
+ * @param {string} [options.stripeWebhookSecret]
88
+ * @param {string} [options.lemonSqueezyApiKey]
89
+ * @param {string} [options.lemonSqueezyWebhookSecret]
90
+ * @param {string} [options.lemonSqueezyStoreId]
91
+ * @returns {PaymentAdapter}
92
+ */
93
+ export function createAdapter(options) {
94
+ const { provider } = options;
95
+ if (provider === "stripe") {
96
+ return new StripeAdapter({
97
+ secretKey: options.stripeSecretKey,
98
+ webhookSecret: options.stripeWebhookSecret,
99
+ });
100
+ } else if (provider === "lemonsqueezy") {
101
+ return new LemonSqueezyAdapter({
102
+ apiKey: options.lemonSqueezyApiKey,
103
+ webhookSecret: options.lemonSqueezyWebhookSecret,
104
+ storeId: options.lemonSqueezyStoreId,
105
+ });
106
+ } else {
107
+ throw new PaymentError("Unknown provider", "CONFIG_ERROR");
108
+ }
109
+ }
@@ -0,0 +1,114 @@
1
+ import Stripe from "stripe";
2
+ import { PaymentAdapter } from "./PaymentAdapter.js";
3
+ import { PaymentError } from "../errors/PaymentError.js";
4
+
5
+ export class StripeAdapter extends PaymentAdapter {
6
+ #stripe;
7
+ #webhookSecret;
8
+
9
+ constructor({ secretKey, webhookSecret }) {
10
+ super();
11
+ if (!secretKey) {
12
+ throw new PaymentError("Stripe secret key is required", "CONFIG_ERROR");
13
+ }
14
+ if (!webhookSecret) {
15
+ throw new PaymentError(
16
+ "Stripe webhook secret is required",
17
+ "CONFIG_ERROR",
18
+ );
19
+ }
20
+ this.#stripe = new Stripe(secretKey, { apiVersion: "2024-06-20" });
21
+ this.#webhookSecret = webhookSecret;
22
+ }
23
+
24
+ async createPayment({ amount, currency, metadata = {} }) {
25
+ try {
26
+ const intent = await this.#stripe.paymentIntents.create({
27
+ amount,
28
+ currency,
29
+ metadata,
30
+ automatic_payment_methods: { enabled: true },
31
+ });
32
+ return {
33
+ id: intent.id,
34
+ clientSecret: intent.client_secret,
35
+ status: intent.status,
36
+ amount: intent.amount,
37
+ currency: intent.currency,
38
+ metadata: intent,
39
+ };
40
+ } catch (error) {
41
+ throw new PaymentError(
42
+ `Stripe createPayment failed: ${error.message}`,
43
+ "STRIPE_ERROR",
44
+ );
45
+ }
46
+ }
47
+
48
+ async refundPayment({ paymentId, amount }) {
49
+ try {
50
+ const params = { payment_intent: paymentId };
51
+ if (amount) {
52
+ params.amount = amount;
53
+ }
54
+ const refund = await this.#stripe.refunds.create(params);
55
+ return {
56
+ id: refund.id,
57
+ status: refund.status,
58
+ amount: refund.amount,
59
+ };
60
+ } catch (error) {
61
+ throw new PaymentError(
62
+ `Stripe refundPayment failed: ${error.message}`,
63
+ "STRIPE_ERROR",
64
+ );
65
+ }
66
+ }
67
+
68
+ async getPaymentStatus(paymentId) {
69
+ try {
70
+ const intent = await this.#stripe.paymentIntents.retrieve(paymentId);
71
+ const statusMap = {
72
+ succeeded: "paid",
73
+ requires_payment_method: "pending",
74
+ requires_confirmation: "pending",
75
+ processing: "pending",
76
+ canceled: "failed",
77
+ requires_action: "pending",
78
+ };
79
+ const normalizedStatus = statusMap[intent.status] || "pending";
80
+ return {
81
+ id: intent.id,
82
+ status: normalizedStatus,
83
+ amount: intent.amount,
84
+ currency: intent.currency,
85
+ metadata: intent,
86
+ };
87
+ } catch (error) {
88
+ throw new PaymentError(
89
+ `Stripe getPaymentStatus failed: ${error.message}`,
90
+ "STRIPE_ERROR",
91
+ );
92
+ }
93
+ }
94
+
95
+ async verifyWebhook(rawBody, signature) {
96
+ try {
97
+ const event = this.#stripe.webhooks.constructEvent(
98
+ rawBody,
99
+ signature,
100
+ this.#webhookSecret,
101
+ );
102
+ return {
103
+ event: event.type,
104
+ data: event.data.object,
105
+ };
106
+ } catch (error) {
107
+ throw new PaymentError(
108
+ "Invalid webhook signature",
109
+ "WEBHOOK_INVALID",
110
+ 401,
111
+ );
112
+ }
113
+ }
114
+ }
@@ -0,0 +1,48 @@
1
+ import * as paymentsService from "../services/payments.service.js";
2
+ import {
3
+ createPaymentSchema,
4
+ refundPaymentSchema,
5
+ } from "../schemas/payments.schemas.js";
6
+ import { extractSignature } from "../helpers/webhookUtils.js";
7
+ import { PaymentError } from "../errors/PaymentError.js";
8
+
9
+ export const createPayment = async (req, res, next) => {
10
+ try {
11
+ const validated = createPaymentSchema.parse(req.body);
12
+ const result = await paymentsService.createPayment(validated);
13
+ res.status(201).json({ success: true, data: result });
14
+ } catch (err) {
15
+ next(err);
16
+ }
17
+ };
18
+
19
+ export const refundPayment = async (req, res, next) => {
20
+ try {
21
+ const validated = refundPaymentSchema.parse(req.body);
22
+ const result = await paymentsService.refundPayment(validated);
23
+ res.json({ success: true, data: result });
24
+ } catch (err) {
25
+ next(err);
26
+ }
27
+ };
28
+
29
+ export const getPaymentStatus = async (req, res, next) => {
30
+ try {
31
+ const result = await paymentsService.getPaymentStatus(req.params.paymentId);
32
+ res.json({ success: true, data: result });
33
+ } catch (err) {
34
+ next(err);
35
+ }
36
+ };
37
+
38
+ export const handleWebhook = async (req, res, next) => {
39
+ try {
40
+ const provider = process.env.PAYMENT_PROVIDER;
41
+ const signature = extractSignature(req, provider);
42
+ const result = await paymentsService.processWebhook(req.body, signature);
43
+ console.log(`Webhook received: ${result.event}`);
44
+ res.status(200).json({ received: true });
45
+ } catch (err) {
46
+ next(err);
47
+ }
48
+ };
@@ -0,0 +1,8 @@
1
+ export class PaymentError extends Error {
2
+ constructor(message, code = "PAYMENT_ERROR", statusCode = 400) {
3
+ super(message);
4
+ this.name = "PaymentError";
5
+ this.code = code;
6
+ this.statusCode = statusCode;
7
+ }
8
+ }
@@ -0,0 +1,27 @@
1
+ import { PaymentError } from "../errors/PaymentError.js";
2
+
3
+ export function getWebhookSignatureHeader(provider) {
4
+ if (provider === "stripe") {
5
+ return "stripe-signature";
6
+ } else if (provider === "lemonsqueezy") {
7
+ return "x-signature";
8
+ } else {
9
+ throw new PaymentError(
10
+ "Unknown provider for webhook header",
11
+ "CONFIG_ERROR",
12
+ );
13
+ }
14
+ }
15
+
16
+ export function extractSignature(req, provider) {
17
+ const headerName = getWebhookSignatureHeader(provider);
18
+ const signature = req.headers[headerName];
19
+ if (!signature) {
20
+ throw new PaymentError(
21
+ "Webhook signature header missing",
22
+ "WEBHOOK_INVALID",
23
+ 401,
24
+ );
25
+ }
26
+ return signature;
27
+ }
@@ -0,0 +1,87 @@
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
+ }
57
+
58
+ export declare function setupPayments(
59
+ app: any,
60
+ options?: SetupPaymentsOptions,
61
+ ): void;
62
+ export declare function createAdapter(
63
+ options: SetupPaymentsOptions,
64
+ ): PaymentAdapter;
65
+ export declare class StripeAdapter implements PaymentAdapter {
66
+ constructor(options: { secretKey: string; webhookSecret: string });
67
+ createPayment(params: CreatePaymentParams): Promise<CreatePaymentResult>;
68
+ refundPayment(params: RefundParams): Promise<RefundResult>;
69
+ getPaymentStatus(paymentId: string): Promise<PaymentStatus>;
70
+ verifyWebhook(rawBody: Buffer, signature: string): Promise<WebhookResult>;
71
+ }
72
+ export declare class LemonSqueezyAdapter implements PaymentAdapter {
73
+ constructor(options: {
74
+ apiKey: string;
75
+ webhookSecret: string;
76
+ storeId: string;
77
+ });
78
+ createPayment(params: CreatePaymentParams): Promise<CreatePaymentResult>;
79
+ refundPayment(params: RefundParams): Promise<RefundResult>;
80
+ getPaymentStatus(paymentId: string): Promise<PaymentStatus>;
81
+ verifyWebhook(rawBody: Buffer, signature: string): Promise<WebhookResult>;
82
+ }
83
+ export declare class PaymentError extends Error {
84
+ code: string;
85
+ statusCode: number;
86
+ }
87
+ export declare function resetAdapter(): void;
@@ -0,0 +1,6 @@
1
+ export { setupPayments } from "./routes/payments.routes.js";
2
+ export { createAdapter } from "./adapters/PaymentAdapter.js";
3
+ export { StripeAdapter } from "./adapters/StripeAdapter.js";
4
+ export { LemonSqueezyAdapter } from "./adapters/LemonSqueezyAdapter.js";
5
+ export { PaymentError } from "./errors/PaymentError.js";
6
+ export { resetAdapter } from "./services/payments.service.js";
@@ -0,0 +1,41 @@
1
+ import { Router } from "express";
2
+ import express from "express";
3
+ import * as controller from "../controllers/payments.controller.js";
4
+
5
+ const router = Router();
6
+
7
+ router.post("/create-intent", controller.createPayment);
8
+ router.post("/refund", controller.refundPayment);
9
+ router.get("/status/:paymentId", controller.getPaymentStatus);
10
+ router.post("/webhook", controller.handleWebhook);
11
+
12
+ export default router;
13
+
14
+ export function setupPayments(app, options = {}) {
15
+ const {
16
+ provider,
17
+ stripeSecretKey,
18
+ stripeWebhookSecret,
19
+ lemonSqueezyApiKey,
20
+ lemonSqueezyWebhookSecret,
21
+ lemonSqueezyStoreId,
22
+ mountPath = "/payments",
23
+ } = options;
24
+
25
+ // Set env vars if provided
26
+ if (provider) process.env.PAYMENT_PROVIDER = provider;
27
+ if (stripeSecretKey) process.env.STRIPE_SECRET_KEY = stripeSecretKey;
28
+ if (stripeWebhookSecret)
29
+ process.env.STRIPE_WEBHOOK_SECRET = stripeWebhookSecret;
30
+ if (lemonSqueezyApiKey) process.env.LEMONSQUEEZY_API_KEY = lemonSqueezyApiKey;
31
+ if (lemonSqueezyWebhookSecret)
32
+ process.env.LEMONSQUEEZY_WEBHOOK_SECRET = lemonSqueezyWebhookSecret;
33
+ if (lemonSqueezyStoreId)
34
+ process.env.LEMONSQUEEZY_STORE_ID = lemonSqueezyStoreId;
35
+
36
+ // Register raw body middleware for webhook BEFORE mounting router
37
+ app.use(`${mountPath}/webhook`, express.raw({ type: "application/json" }));
38
+
39
+ // Mount the router
40
+ app.use(mountPath, router);
41
+ }
@@ -0,0 +1,24 @@
1
+ import { z } from "zod";
2
+
3
+ export const createPaymentSchema = z.object({
4
+ amount: z
5
+ .number({ required_error: "amount is required" })
6
+ .int({ message: "amount must be an integer" })
7
+ .positive({ message: "amount must be positive" })
8
+ .max(99999999, "amount exceeds maximum allowed"),
9
+
10
+ currency: z
11
+ .string({ required_error: "currency is required" })
12
+ .length(3, "currency must be 3 characters")
13
+ .toLowerCase(),
14
+
15
+ metadata: z.record(z.string()).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.number().int().positive().optional(),
24
+ });
@@ -0,0 +1,68 @@
1
+ import { StripeAdapter } from "../adapters/StripeAdapter.js";
2
+ import { LemonSqueezyAdapter } from "../adapters/LemonSqueezyAdapter.js";
3
+ import { PaymentError } from "../errors/PaymentError.js";
4
+
5
+ let adapter = null;
6
+ const processedWebhookIds = new Set();
7
+
8
+ export function getAdapter() {
9
+ if (adapter) return adapter;
10
+
11
+ const provider = process.env.PAYMENT_PROVIDER;
12
+ if (!provider) {
13
+ throw new PaymentError(
14
+ "PAYMENT_PROVIDER environment variable is required",
15
+ "PROVIDER_NOT_CONFIGURED",
16
+ );
17
+ }
18
+
19
+ if (provider === "stripe") {
20
+ adapter = new StripeAdapter({
21
+ secretKey: process.env.STRIPE_SECRET_KEY,
22
+ webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
23
+ });
24
+ } else if (provider === "lemonsqueezy") {
25
+ adapter = new LemonSqueezyAdapter({
26
+ apiKey: process.env.LEMONSQUEEZY_API_KEY,
27
+ webhookSecret: process.env.LEMONSQUEEZY_WEBHOOK_SECRET,
28
+ storeId: process.env.LEMONSQUEEZY_STORE_ID,
29
+ });
30
+ } else {
31
+ throw new PaymentError(`Unknown provider: ${provider}`, "CONFIG_ERROR");
32
+ }
33
+
34
+ return adapter;
35
+ }
36
+
37
+ export function resetAdapter() {
38
+ adapter = null;
39
+ }
40
+
41
+ export async function createPayment({ amount, currency, metadata }) {
42
+ const adapter = getAdapter();
43
+ return adapter.createPayment({ amount, currency, metadata });
44
+ }
45
+
46
+ export async function refundPayment({ paymentId, amount }) {
47
+ const adapter = getAdapter();
48
+ return adapter.refundPayment({ paymentId, amount });
49
+ }
50
+
51
+ export async function getPaymentStatus(paymentId) {
52
+ const adapter = getAdapter();
53
+ return adapter.getPaymentStatus(paymentId);
54
+ }
55
+
56
+ export async function processWebhook(rawBody, signature) {
57
+ const adapter = getAdapter();
58
+ const { event, data } = await adapter.verifyWebhook(rawBody, signature);
59
+
60
+ const eventId = data.id ?? `${event}-${Date.now()}`;
61
+
62
+ if (processedWebhookIds.has(eventId)) {
63
+ return { event, data, duplicate: true };
64
+ }
65
+
66
+ processedWebhookIds.add(eventId);
67
+ return { event, data, duplicate: false };
68
+ }