@reeboot/strapi-payment-plugin 0.0.1 → 0.0.3
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/README.md +378 -119
- package/dist/_chunks/Analytics-DSJqY9ng.mjs +355 -0
- package/dist/_chunks/Analytics-nBSdLT2v.js +355 -0
- package/dist/_chunks/App-B83DZ9NG.js +70 -0
- package/dist/_chunks/App-BUSTbkyy.mjs +68 -0
- package/dist/_chunks/Customers-BpFzfglV.js +273 -0
- package/dist/_chunks/Customers-C6FH7-zG.mjs +273 -0
- package/dist/_chunks/Dashboard-CNMTzSyc.js +180 -0
- package/dist/_chunks/Dashboard-Dbwl0ZBo.mjs +180 -0
- package/dist/_chunks/Orders-CBkT2YfP.mjs +308 -0
- package/dist/_chunks/Orders-OG-pwV-B.js +308 -0
- package/dist/_chunks/Payments-BLen1P9N.js +489 -0
- package/dist/_chunks/Payments-DSDJ-HWm.mjs +489 -0
- package/dist/_chunks/Settings-Dq1xy32B.js +357 -0
- package/dist/_chunks/Settings-jmGslDsB.mjs +357 -0
- package/dist/_chunks/en-BJocyOVu.mjs +240 -0
- package/dist/_chunks/en-BkVAf_R4.js +240 -0
- package/dist/_chunks/index-BqqrpI6D.js +66 -0
- package/dist/_chunks/index-DS_PYNkf.mjs +67 -0
- package/dist/admin/index.js +2 -63
- package/dist/admin/index.mjs +2 -63
- package/dist/admin/src/components/AnalyticsChart.d.ts +19 -0
- package/dist/admin/src/components/CustomerList.d.ts +21 -0
- package/dist/admin/src/components/OrderList.d.ts +27 -0
- package/dist/admin/src/components/PaymentCard.d.ts +39 -0
- package/dist/admin/src/components/PaymentList.d.ts +19 -0
- package/dist/admin/src/components/RefundModal.d.ts +15 -0
- package/dist/admin/src/pages/Analytics.d.ts +2 -0
- package/dist/admin/src/pages/Customers.d.ts +2 -0
- package/dist/admin/src/pages/Dashboard.d.ts +2 -0
- package/dist/admin/src/pages/HomePage.d.ts +1 -1
- package/dist/admin/src/pages/Orders.d.ts +2 -0
- package/dist/admin/src/pages/Payments.d.ts +2 -0
- package/dist/admin/src/pages/Settings.d.ts +2 -0
- package/dist/admin/src/pluginId.d.ts +1 -1
- package/dist/server/index.js +1770 -992
- package/dist/server/index.mjs +1773 -995
- package/dist/server/src/bootstrap.d.ts +5 -11
- package/dist/server/src/config/index.d.ts +0 -10
- package/dist/server/src/content-types/customer/index.d.ts +69 -0
- package/dist/server/src/content-types/index.d.ts +123 -39
- package/dist/server/src/content-types/{product.d.ts → order/index.d.ts} +26 -19
- package/dist/server/src/content-types/{subscription.d.ts → payment/index.d.ts} +30 -21
- package/dist/server/src/controllers/controller.d.ts +5 -12
- package/dist/server/src/controllers/index.d.ts +29 -34
- package/dist/server/src/controllers/stripe.d.ts +104 -0
- package/dist/server/src/index.d.ts +179 -139
- package/dist/server/src/middlewares/index.d.ts +19 -1
- package/dist/server/src/policies/index.d.ts +3 -1
- package/dist/server/src/routes/{admin-routes.d.ts → admin/index.d.ts} +4 -4
- package/dist/server/src/routes/content-api/index.d.ts +21 -0
- package/dist/server/src/routes/index.d.ts +11 -16
- package/dist/server/src/services/index.d.ts +2 -38
- package/dist/server/src/services/{stripeDriver.d.ts → stripe.d.ts} +52 -59
- package/dist/server/src/types/index.d.ts +179 -0
- package/package.json +20 -25
- package/dist/_chunks/App-DD7GyuRr.mjs +0 -1424
- package/dist/_chunks/App-KZVBFRwo.js +0 -1424
- package/dist/_chunks/en-B4KWt_jN.js +0 -4
- package/dist/_chunks/en-Byx4XI2L.mjs +0 -4
- package/dist/admin/src/components/Header.d.ts +0 -2
- package/dist/admin/src/components/NavigationMenu.d.ts +0 -2
- package/dist/admin/src/components/Sidebar.d.ts +0 -2
- package/dist/admin/src/components/TransactionDetailsModal.d.ts +0 -18
- package/dist/admin/src/components/TransactionList.d.ts +0 -18
- package/dist/admin/src/pages/ConfigurationPage.d.ts +0 -2
- package/dist/admin/src/pages/DashboardPage.d.ts +0 -2
- package/dist/admin/src/pages/ProductsPage.d.ts +0 -2
- package/dist/admin/src/pages/SubscriptionsPage.d.ts +0 -2
- package/dist/admin/src/pages/TransactionsPage.d.ts +0 -2
- package/dist/server/src/controllers/product.d.ts +0 -18
- package/dist/server/src/controllers/subscription.d.ts +0 -16
- package/dist/server/src/controllers/webhook.d.ts +0 -10
- package/dist/server/src/routes/content-api.d.ts +0 -12
- package/dist/server/src/routes/product.d.ts +0 -2
- package/dist/server/src/routes/refund-routes.d.ts +0 -13
- package/dist/server/src/routes/subscription.d.ts +0 -5
- package/dist/server/src/routes/webhook.d.ts +0 -15
- package/dist/server/src/services/paypalDriver.d.ts +0 -47
- package/dist/server/src/services/product.d.ts +0 -7
- package/dist/server/src/services/service.d.ts +0 -26
- package/dist/server/src/services/subscription.d.ts +0 -9
- package/dist/server/src/services/sync.d.ts +0 -13
- package/jest.config.js +0 -13
package/dist/server/index.mjs
CHANGED
|
@@ -1,1237 +1,2015 @@
|
|
|
1
1
|
import Stripe from "stripe";
|
|
2
|
-
|
|
3
|
-
const bootstrap = async ({ strapi }) => {
|
|
4
|
-
const syncService = strapi.service("plugin::strapi-payment-plugin.sync");
|
|
5
|
-
syncService?.startPeriodicSync();
|
|
6
|
-
console.log("Periodic sync started");
|
|
2
|
+
const bootstrap = ({ strapi: strapi2 }) => {
|
|
7
3
|
};
|
|
8
|
-
const destroy = ({ strapi }) => {
|
|
4
|
+
const destroy = ({ strapi: strapi2 }) => {
|
|
9
5
|
};
|
|
10
|
-
const register = ({ strapi }) => {
|
|
6
|
+
const register = ({ strapi: strapi2 }) => {
|
|
11
7
|
};
|
|
12
8
|
const config = {
|
|
13
9
|
default: {},
|
|
14
10
|
validator() {
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
const schema$2 = {
|
|
14
|
+
kind: "collectionType",
|
|
15
|
+
collectionName: "customers",
|
|
16
|
+
info: {
|
|
17
|
+
singularName: "customer",
|
|
18
|
+
pluralName: "customers",
|
|
19
|
+
displayName: "Customer",
|
|
20
|
+
description: "Customer information for payment processing"
|
|
15
21
|
},
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
options: {
|
|
23
|
+
draftAndPublish: false
|
|
24
|
+
},
|
|
25
|
+
pluginOptions: {
|
|
26
|
+
i18n: {
|
|
27
|
+
localized: true
|
|
28
|
+
}
|
|
20
29
|
},
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
attributes: {
|
|
31
|
+
email: {
|
|
32
|
+
type: "email",
|
|
33
|
+
unique: true,
|
|
34
|
+
required: true,
|
|
35
|
+
configurable: false
|
|
36
|
+
},
|
|
37
|
+
stripe_customer_id: {
|
|
38
|
+
type: "string",
|
|
39
|
+
unique: true,
|
|
40
|
+
required: true,
|
|
41
|
+
configurable: false
|
|
42
|
+
},
|
|
43
|
+
first_name: {
|
|
44
|
+
type: "string",
|
|
45
|
+
required: true,
|
|
46
|
+
configurable: false
|
|
47
|
+
},
|
|
48
|
+
last_name: {
|
|
49
|
+
type: "string",
|
|
50
|
+
required: true,
|
|
51
|
+
configurable: false
|
|
52
|
+
},
|
|
53
|
+
phone: {
|
|
54
|
+
type: "string",
|
|
55
|
+
configurable: false
|
|
56
|
+
},
|
|
57
|
+
address: {
|
|
58
|
+
type: "json",
|
|
59
|
+
configurable: false
|
|
60
|
+
},
|
|
61
|
+
metadata: {
|
|
62
|
+
type: "json",
|
|
63
|
+
configurable: false
|
|
64
|
+
},
|
|
65
|
+
orders: {
|
|
66
|
+
type: "relation",
|
|
67
|
+
relation: "oneToMany",
|
|
68
|
+
target: "plugin::payment-plugin.order",
|
|
69
|
+
mappedBy: "customer"
|
|
70
|
+
},
|
|
71
|
+
payments: {
|
|
72
|
+
type: "relation",
|
|
73
|
+
relation: "oneToMany",
|
|
74
|
+
target: "plugin::payment-plugin.payment",
|
|
75
|
+
mappedBy: "customer"
|
|
76
|
+
}
|
|
25
77
|
}
|
|
26
78
|
};
|
|
79
|
+
const customer = { schema: schema$2 };
|
|
27
80
|
const schema$1 = {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
81
|
+
kind: "collectionType",
|
|
82
|
+
collectionName: "orders",
|
|
83
|
+
info: {
|
|
84
|
+
singularName: "order",
|
|
85
|
+
pluralName: "orders",
|
|
86
|
+
displayName: "Order",
|
|
87
|
+
description: "Customer orders linked to payments"
|
|
35
88
|
},
|
|
36
|
-
|
|
37
|
-
|
|
89
|
+
options: {
|
|
90
|
+
draftAndPublish: false
|
|
38
91
|
},
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
},
|
|
43
|
-
"content-type-builder": {
|
|
44
|
-
"visible": true
|
|
92
|
+
pluginOptions: {
|
|
93
|
+
i18n: {
|
|
94
|
+
localized: true
|
|
45
95
|
}
|
|
46
96
|
},
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"type": "text"
|
|
54
|
-
},
|
|
55
|
-
"price": {
|
|
56
|
-
"type": "decimal",
|
|
57
|
-
"required": true
|
|
97
|
+
attributes: {
|
|
98
|
+
order_number: {
|
|
99
|
+
type: "string",
|
|
100
|
+
unique: true,
|
|
101
|
+
required: true,
|
|
102
|
+
configurable: false
|
|
58
103
|
},
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
104
|
+
total_amount: {
|
|
105
|
+
type: "integer",
|
|
106
|
+
required: true,
|
|
107
|
+
min: 0,
|
|
108
|
+
configurable: false,
|
|
109
|
+
default: 0
|
|
63
110
|
},
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
111
|
+
currency: {
|
|
112
|
+
type: "string",
|
|
113
|
+
required: true,
|
|
114
|
+
default: "usd",
|
|
115
|
+
configurable: false
|
|
67
116
|
},
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
117
|
+
status: {
|
|
118
|
+
type: "enumeration",
|
|
119
|
+
enum: ["pending", "processing", "completed", "failed", "refunded"],
|
|
120
|
+
required: true,
|
|
121
|
+
default: "pending",
|
|
122
|
+
configurable: false
|
|
72
123
|
},
|
|
73
|
-
|
|
74
|
-
|
|
124
|
+
customer: {
|
|
125
|
+
type: "relation",
|
|
126
|
+
relation: "manyToOne",
|
|
127
|
+
target: "plugin::payment-plugin.customer",
|
|
128
|
+
inversedBy: "orders"
|
|
75
129
|
},
|
|
76
|
-
|
|
77
|
-
|
|
130
|
+
payments: {
|
|
131
|
+
type: "relation",
|
|
132
|
+
relation: "oneToMany",
|
|
133
|
+
target: "plugin::payment-plugin.payment",
|
|
134
|
+
mappedBy: "order"
|
|
78
135
|
},
|
|
79
|
-
|
|
80
|
-
|
|
136
|
+
items: {
|
|
137
|
+
type: "json",
|
|
138
|
+
required: true,
|
|
139
|
+
configurable: false
|
|
81
140
|
},
|
|
82
|
-
|
|
83
|
-
|
|
141
|
+
metadata: {
|
|
142
|
+
type: "json",
|
|
143
|
+
configurable: false
|
|
84
144
|
}
|
|
85
145
|
}
|
|
86
146
|
};
|
|
87
|
-
const
|
|
88
|
-
schema: schema$1
|
|
89
|
-
};
|
|
147
|
+
const order = { schema: schema$1 };
|
|
90
148
|
const schema = {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
149
|
+
kind: "collectionType",
|
|
150
|
+
collectionName: "payments",
|
|
151
|
+
info: {
|
|
152
|
+
singularName: "payment",
|
|
153
|
+
pluralName: "payments",
|
|
154
|
+
displayName: "Payment",
|
|
155
|
+
description: "Payment records for Stripe payment processing"
|
|
98
156
|
},
|
|
99
|
-
|
|
100
|
-
|
|
157
|
+
options: {
|
|
158
|
+
draftAndPublish: false
|
|
101
159
|
},
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
},
|
|
106
|
-
"content-type-builder": {
|
|
107
|
-
"visible": true
|
|
160
|
+
pluginOptions: {
|
|
161
|
+
i18n: {
|
|
162
|
+
localized: true
|
|
108
163
|
}
|
|
109
164
|
},
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
165
|
+
attributes: {
|
|
166
|
+
stripe_payment_intent_id: {
|
|
167
|
+
type: "string",
|
|
168
|
+
unique: true,
|
|
169
|
+
required: true,
|
|
170
|
+
configurable: false
|
|
115
171
|
},
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
172
|
+
amount: {
|
|
173
|
+
type: "integer",
|
|
174
|
+
required: true,
|
|
175
|
+
min: 0,
|
|
176
|
+
configurable: false,
|
|
177
|
+
default: 0
|
|
120
178
|
},
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
179
|
+
currency: {
|
|
180
|
+
type: "string",
|
|
181
|
+
required: true,
|
|
182
|
+
default: "usd",
|
|
183
|
+
configurable: false
|
|
125
184
|
},
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
"
|
|
185
|
+
status: {
|
|
186
|
+
type: "enumeration",
|
|
187
|
+
enum: ["pending", "succeeded", "failed", "canceled"],
|
|
188
|
+
required: true,
|
|
189
|
+
default: "pending",
|
|
190
|
+
configurable: false
|
|
129
191
|
},
|
|
130
|
-
|
|
131
|
-
|
|
192
|
+
payment_method: {
|
|
193
|
+
type: "string",
|
|
194
|
+
configurable: false
|
|
132
195
|
},
|
|
133
|
-
|
|
134
|
-
|
|
196
|
+
metadata: {
|
|
197
|
+
type: "json",
|
|
198
|
+
configurable: false
|
|
135
199
|
},
|
|
136
|
-
|
|
137
|
-
|
|
200
|
+
customer: {
|
|
201
|
+
type: "relation",
|
|
202
|
+
relation: "manyToOne",
|
|
203
|
+
target: "plugin::payment-plugin.customer",
|
|
204
|
+
inversedBy: "payments"
|
|
138
205
|
},
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
206
|
+
order: {
|
|
207
|
+
type: "relation",
|
|
208
|
+
relation: "manyToOne",
|
|
209
|
+
target: "plugin::payment-plugin.order",
|
|
210
|
+
inversedBy: "payments"
|
|
144
211
|
}
|
|
145
212
|
}
|
|
146
213
|
};
|
|
147
|
-
const
|
|
148
|
-
schema
|
|
149
|
-
};
|
|
214
|
+
const payment = { schema };
|
|
150
215
|
const contentTypes = {
|
|
151
|
-
|
|
152
|
-
|
|
216
|
+
customer,
|
|
217
|
+
order,
|
|
218
|
+
payment
|
|
153
219
|
};
|
|
154
|
-
const
|
|
155
|
-
|
|
220
|
+
const controller = ({ strapi: strapi2 }) => ({
|
|
221
|
+
index(ctx) {
|
|
222
|
+
ctx.body = strapi2.plugin("payment-plugin").service("service").getWelcomeMessage();
|
|
223
|
+
}
|
|
156
224
|
});
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const event = stripe.webhooks.constructEvent(
|
|
170
|
-
payload,
|
|
171
|
-
signature,
|
|
172
|
-
config.stripe.webhookSecret
|
|
173
|
-
);
|
|
174
|
-
return event;
|
|
175
|
-
},
|
|
176
|
-
async refundPayment(transactionId, amount, options) {
|
|
177
|
-
return stripe.refunds.create({
|
|
178
|
-
payment_intent: transactionId,
|
|
179
|
-
amount,
|
|
180
|
-
...options
|
|
181
|
-
});
|
|
182
|
-
},
|
|
183
|
-
async getStripeProducts() {
|
|
184
|
-
const products = await stripe.products.list({
|
|
185
|
-
limit: 100,
|
|
186
|
-
expand: ["data.default_price"]
|
|
187
|
-
});
|
|
188
|
-
return products.data;
|
|
189
|
-
},
|
|
190
|
-
async getStripeProduct(productId) {
|
|
191
|
-
const product2 = await stripe.products.retrieve(productId, {
|
|
192
|
-
expand: ["default_price"]
|
|
193
|
-
});
|
|
194
|
-
return product2;
|
|
195
|
-
},
|
|
196
|
-
async getProductPrice(productId) {
|
|
197
|
-
const product2 = await this.getStripeProduct(productId);
|
|
198
|
-
if (product2.default_price) {
|
|
199
|
-
if (typeof product2.default_price === "string") {
|
|
200
|
-
const price = await stripe.prices.retrieve(product2.default_price);
|
|
201
|
-
return {
|
|
202
|
-
unit_amount: price.unit_amount,
|
|
203
|
-
currency: price.currency
|
|
204
|
-
};
|
|
205
|
-
} else {
|
|
206
|
-
return {
|
|
207
|
-
unit_amount: product2.default_price.unit_amount,
|
|
208
|
-
currency: product2.default_price.currency
|
|
209
|
-
};
|
|
225
|
+
const stripeController = {
|
|
226
|
+
/**
|
|
227
|
+
* Create a payment intent
|
|
228
|
+
*/
|
|
229
|
+
async createPaymentIntent(ctx) {
|
|
230
|
+
try {
|
|
231
|
+
const { amount, currency, customerId, orderId, metadata = {} } = ctx.request.body;
|
|
232
|
+
if (!amount || !currency) {
|
|
233
|
+
return ctx.badRequest("Amount and currency are required");
|
|
234
|
+
}
|
|
235
|
+
if (amount <= 0) {
|
|
236
|
+
return ctx.badRequest("Amount must be greater than 0");
|
|
210
237
|
}
|
|
238
|
+
const stripeService2 = strapi.plugin("payment-plugin").service("stripe");
|
|
239
|
+
const paymentIntent = await stripeService2.createPaymentIntent({
|
|
240
|
+
amount,
|
|
241
|
+
currency,
|
|
242
|
+
metadata,
|
|
243
|
+
customerId,
|
|
244
|
+
orderId
|
|
245
|
+
});
|
|
246
|
+
ctx.body = {
|
|
247
|
+
success: true,
|
|
248
|
+
data: {
|
|
249
|
+
paymentIntentId: paymentIntent.id,
|
|
250
|
+
clientSecret: paymentIntent.client_secret,
|
|
251
|
+
status: paymentIntent.status,
|
|
252
|
+
amount: paymentIntent.amount,
|
|
253
|
+
currency: paymentIntent.currency
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
} catch (error) {
|
|
257
|
+
strapi.log.error("Failed to create payment intent", { error });
|
|
258
|
+
ctx.internalServerError("Failed to create payment intent");
|
|
211
259
|
}
|
|
212
|
-
return null;
|
|
213
260
|
},
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
async
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
currency: productData.currency || "usd",
|
|
223
|
-
unit_amount: Math.round(productData.price * 100)
|
|
224
|
-
// Convert to cents
|
|
261
|
+
/**
|
|
262
|
+
* Confirm a payment intent
|
|
263
|
+
*/
|
|
264
|
+
async confirmPayment(ctx) {
|
|
265
|
+
try {
|
|
266
|
+
const { paymentIntentId } = ctx.params;
|
|
267
|
+
if (!paymentIntentId) {
|
|
268
|
+
return ctx.badRequest("Payment intent ID is required");
|
|
225
269
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
customer: customerId,
|
|
241
|
-
items: [{ price: priceId }],
|
|
242
|
-
...options
|
|
243
|
-
});
|
|
244
|
-
},
|
|
245
|
-
async updateSubscription(subscriptionId, options = {}) {
|
|
246
|
-
return stripe.subscriptions.update(subscriptionId, options);
|
|
247
|
-
},
|
|
248
|
-
async cancelSubscription(subscriptionId) {
|
|
249
|
-
return stripe.subscriptions.cancel(subscriptionId);
|
|
250
|
-
},
|
|
251
|
-
async getSubscription(subscriptionId) {
|
|
252
|
-
return stripe.subscriptions.retrieve(subscriptionId);
|
|
253
|
-
},
|
|
254
|
-
async getTransactions(limit = 10, startingAfter = null) {
|
|
255
|
-
const params = {
|
|
256
|
-
limit: Math.min(limit, 100)
|
|
257
|
-
// Max 100 per Stripe API
|
|
258
|
-
};
|
|
259
|
-
if (startingAfter) {
|
|
260
|
-
params.starting_after = startingAfter;
|
|
270
|
+
const stripeService2 = strapi.plugin("payment-plugin").service("stripe");
|
|
271
|
+
const paymentIntent = await stripeService2.confirmPayment(paymentIntentId);
|
|
272
|
+
ctx.body = {
|
|
273
|
+
success: true,
|
|
274
|
+
data: {
|
|
275
|
+
paymentIntentId: paymentIntent.id,
|
|
276
|
+
status: paymentIntent.status,
|
|
277
|
+
amount: paymentIntent.amount,
|
|
278
|
+
currency: paymentIntent.currency
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
} catch (error) {
|
|
282
|
+
strapi.log.error("Failed to confirm payment", { error });
|
|
283
|
+
ctx.internalServerError("Failed to confirm payment");
|
|
261
284
|
}
|
|
262
|
-
const payments = await stripe.paymentIntents.list(params);
|
|
263
|
-
return payments.data;
|
|
264
285
|
},
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if (
|
|
272
|
-
|
|
273
|
-
priceInfo = {
|
|
274
|
-
unit_amount: price.unit_amount,
|
|
275
|
-
currency: price.currency
|
|
276
|
-
};
|
|
277
|
-
} else {
|
|
278
|
-
priceInfo = {
|
|
279
|
-
unit_amount: product2.default_price.unit_amount,
|
|
280
|
-
currency: product2.default_price.currency
|
|
281
|
-
};
|
|
286
|
+
/**
|
|
287
|
+
* Create a Stripe customer
|
|
288
|
+
*/
|
|
289
|
+
async createCustomer(ctx) {
|
|
290
|
+
try {
|
|
291
|
+
const { email, firstName, lastName, phone, address, metadata = {} } = ctx.request.body;
|
|
292
|
+
if (!email || !firstName || !lastName) {
|
|
293
|
+
return ctx.badRequest("Email, first name, and last name are required");
|
|
282
294
|
}
|
|
295
|
+
const stripeService2 = strapi.plugin("payment-plugin").service("stripe");
|
|
296
|
+
const customer2 = await stripeService2.createCustomer({
|
|
297
|
+
email,
|
|
298
|
+
firstName,
|
|
299
|
+
lastName,
|
|
300
|
+
phone,
|
|
301
|
+
address,
|
|
302
|
+
metadata
|
|
303
|
+
});
|
|
304
|
+
ctx.body = {
|
|
305
|
+
success: true,
|
|
306
|
+
data: {
|
|
307
|
+
customerId: customer2.id,
|
|
308
|
+
email: customer2.email,
|
|
309
|
+
name: customer2.name,
|
|
310
|
+
created: customer2.created
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
} catch (error) {
|
|
314
|
+
strapi.log.error("Failed to create customer", { error });
|
|
315
|
+
ctx.internalServerError("Failed to create customer");
|
|
283
316
|
}
|
|
284
|
-
return {
|
|
285
|
-
name: product2.name,
|
|
286
|
-
description: product2.description,
|
|
287
|
-
price: priceInfo ? priceInfo.unit_amount / 100 : 0,
|
|
288
|
-
currency: priceInfo ? priceInfo.currency : "USD",
|
|
289
|
-
stripeProductId: product2.id
|
|
290
|
-
};
|
|
291
|
-
},
|
|
292
|
-
async createPayment(amount, currency, paymentMethod, returnUrl = "https://example.com/return") {
|
|
293
|
-
const paymentIntent = await stripe.paymentIntents.create({
|
|
294
|
-
amount,
|
|
295
|
-
currency,
|
|
296
|
-
payment_method: paymentMethod,
|
|
297
|
-
confirmation_method: "automatic",
|
|
298
|
-
confirm: true,
|
|
299
|
-
return_url: returnUrl
|
|
300
|
-
});
|
|
301
|
-
return {
|
|
302
|
-
paymentIntent,
|
|
303
|
-
redirectUrl: paymentIntent.next_action?.redirect_to_url?.url || null
|
|
304
|
-
};
|
|
305
317
|
},
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
318
|
+
/**
|
|
319
|
+
* Retrieve a Stripe customer
|
|
320
|
+
*/
|
|
321
|
+
async retrieveCustomer(ctx) {
|
|
322
|
+
try {
|
|
323
|
+
const { customerId } = ctx.params;
|
|
324
|
+
if (!customerId) {
|
|
325
|
+
return ctx.badRequest("Customer ID is required");
|
|
326
|
+
}
|
|
327
|
+
const stripeService2 = strapi.plugin("payment-plugin").service("stripe");
|
|
328
|
+
const customer2 = await stripeService2.retrieveCustomer(customerId);
|
|
329
|
+
if ("deleted" in customer2 && customer2.deleted) {
|
|
330
|
+
return ctx.notFound("Customer not found");
|
|
331
|
+
}
|
|
332
|
+
ctx.body = {
|
|
333
|
+
success: true,
|
|
334
|
+
data: {
|
|
335
|
+
customerId: customer2.id,
|
|
336
|
+
email: customer2.email,
|
|
337
|
+
name: customer2.name,
|
|
338
|
+
created: customer2.created,
|
|
339
|
+
metadata: customer2.metadata
|
|
340
|
+
}
|
|
319
341
|
};
|
|
342
|
+
} catch (error) {
|
|
343
|
+
strapi.log.error("Failed to retrieve customer", { error });
|
|
344
|
+
ctx.internalServerError("Failed to retrieve customer");
|
|
320
345
|
}
|
|
321
|
-
return {
|
|
322
|
-
paymentIntent,
|
|
323
|
-
redirectUrl: null
|
|
324
|
-
};
|
|
325
346
|
},
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
347
|
+
/**
|
|
348
|
+
* Create a refund
|
|
349
|
+
*/
|
|
350
|
+
async createRefund(ctx) {
|
|
351
|
+
try {
|
|
352
|
+
const { paymentIntentId, amount, reason, metadata = {} } = ctx.request.body;
|
|
353
|
+
if (!paymentIntentId) {
|
|
354
|
+
return ctx.badRequest("Payment intent ID is required");
|
|
355
|
+
}
|
|
356
|
+
const stripeService2 = strapi.plugin("payment-plugin").service("stripe");
|
|
357
|
+
const refund = await stripeService2.createRefund({
|
|
358
|
+
paymentIntentId,
|
|
359
|
+
amount,
|
|
360
|
+
reason,
|
|
361
|
+
metadata
|
|
362
|
+
});
|
|
363
|
+
ctx.body = {
|
|
364
|
+
success: true,
|
|
365
|
+
data: {
|
|
366
|
+
refundId: refund.id,
|
|
367
|
+
amount: refund.amount,
|
|
368
|
+
currency: refund.currency,
|
|
369
|
+
status: refund.status,
|
|
370
|
+
reason: refund.reason
|
|
332
371
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
};
|
|
339
|
-
const controller = ({ strapi }) => ({
|
|
340
|
-
async initiatePayment(ctx) {
|
|
341
|
-
const { gateway, amount, currency, orderId, options } = ctx.request.body;
|
|
342
|
-
const result = await strapi.plugin("strapi-payment-plugin").service("service").initializePayment(gateway, amount, currency, orderId, options);
|
|
343
|
-
ctx.body = result;
|
|
344
|
-
},
|
|
345
|
-
async processWebhook(ctx) {
|
|
346
|
-
const { gateway } = ctx.params;
|
|
347
|
-
const payload = ctx.request.body;
|
|
348
|
-
const signature = ctx.request.headers["stripe-signature"] || ctx.request.headers["paypal-transmission-id"];
|
|
349
|
-
const result = await strapi.plugin("strapi-payment-plugin").service("service").processWebhook(gateway, payload, signature);
|
|
350
|
-
ctx.body = result;
|
|
351
|
-
},
|
|
352
|
-
async refundPayment(ctx) {
|
|
353
|
-
const { gateway, transactionId, amount, options } = ctx.request.body;
|
|
354
|
-
const result = await strapi.plugin("strapi-payment-plugin").service("service").refundPayment(gateway, transactionId, amount, options);
|
|
355
|
-
ctx.body = result;
|
|
356
|
-
},
|
|
357
|
-
async getPaymentDetails(ctx) {
|
|
358
|
-
const { gateway, transactionId } = ctx.params;
|
|
359
|
-
const result = await strapi.plugin("strapi-payment-plugin").service("service").getPaymentDetails(gateway, transactionId);
|
|
360
|
-
ctx.body = result;
|
|
361
|
-
},
|
|
362
|
-
async getTransactions(ctx) {
|
|
363
|
-
console.log("Fetching transactions with params:", ctx.query);
|
|
364
|
-
const { gateway, status, limit = 100, offset = 0 } = ctx.query;
|
|
365
|
-
const result = await strapi.plugin("strapi-payment-plugin").service("service").getTransactions(gateway, status, limit, offset);
|
|
366
|
-
ctx.body = result;
|
|
367
|
-
},
|
|
368
|
-
async saveConfiguration(ctx) {
|
|
369
|
-
const configData = ctx.request.body;
|
|
370
|
-
const result = await strapi.plugin("strapi-payment-plugin").service("service").saveConfiguration(configData);
|
|
371
|
-
ctx.body = result;
|
|
372
|
+
};
|
|
373
|
+
} catch (error) {
|
|
374
|
+
strapi.log.error("Failed to create refund", { error });
|
|
375
|
+
ctx.internalServerError("Failed to create refund");
|
|
376
|
+
}
|
|
372
377
|
},
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
378
|
+
/**
|
|
379
|
+
* Handle Stripe webhooks
|
|
380
|
+
*/
|
|
381
|
+
async handleWebhook(ctx) {
|
|
382
|
+
try {
|
|
383
|
+
const signature = ctx.request.headers["stripe-signature"];
|
|
384
|
+
let payload;
|
|
385
|
+
if (typeof ctx.request.body === "string") {
|
|
386
|
+
payload = ctx.request.body;
|
|
387
|
+
} else if (Buffer.isBuffer(ctx.request.body)) {
|
|
388
|
+
payload = ctx.request.body;
|
|
389
|
+
} else {
|
|
390
|
+
payload = JSON.stringify(ctx.request.body);
|
|
391
|
+
}
|
|
392
|
+
if (!signature) {
|
|
393
|
+
return ctx.badRequest("Missing Stripe signature");
|
|
394
|
+
}
|
|
395
|
+
const stripeService2 = strapi.plugin("payment-plugin").service("stripe");
|
|
396
|
+
const event = await stripeService2.constructEvent(payload, signature);
|
|
397
|
+
await stripeService2.handleWebhook(event);
|
|
398
|
+
ctx.body = { received: true };
|
|
399
|
+
} catch (error) {
|
|
400
|
+
strapi.log.error("Failed to handle webhook", { error });
|
|
401
|
+
ctx.badRequest(`Webhook Error: ${error.message}`);
|
|
402
|
+
}
|
|
377
403
|
},
|
|
378
|
-
|
|
379
|
-
|
|
404
|
+
/**
|
|
405
|
+
* Get Stripe service status/health check
|
|
406
|
+
*/
|
|
407
|
+
async getServiceStatus(ctx) {
|
|
380
408
|
try {
|
|
381
|
-
const
|
|
409
|
+
const stripeService2 = strapi.plugin("payment-plugin").service("stripe");
|
|
410
|
+
let stripe;
|
|
411
|
+
try {
|
|
412
|
+
stripe = await stripeService2.initializeStripe();
|
|
413
|
+
} catch (e) {
|
|
414
|
+
stripe = null;
|
|
415
|
+
}
|
|
382
416
|
ctx.body = {
|
|
383
417
|
success: true,
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
418
|
+
data: {
|
|
419
|
+
service: "Stripe Payment Service",
|
|
420
|
+
status: stripe ? "initialized" : "not initialized",
|
|
421
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
422
|
+
}
|
|
387
423
|
};
|
|
388
424
|
} catch (error) {
|
|
389
|
-
|
|
390
|
-
ctx.
|
|
391
|
-
success: false,
|
|
392
|
-
message: error.message
|
|
393
|
-
};
|
|
394
|
-
ctx.status = 400;
|
|
425
|
+
strapi.log.error("Failed to get service status", { error });
|
|
426
|
+
ctx.internalServerError("Failed to get service status");
|
|
395
427
|
}
|
|
396
428
|
},
|
|
397
|
-
|
|
398
|
-
|
|
429
|
+
/**
|
|
430
|
+
* Get payment details by Stripe payment intent ID
|
|
431
|
+
*/
|
|
432
|
+
async getPaymentDetails(ctx) {
|
|
399
433
|
try {
|
|
400
|
-
const
|
|
401
|
-
|
|
434
|
+
const { id: paymentIntentId } = ctx.params;
|
|
435
|
+
if (!paymentIntentId) {
|
|
436
|
+
return ctx.badRequest("Payment intent ID is required");
|
|
437
|
+
}
|
|
438
|
+
const stripeService2 = strapi.plugin("payment-plugin").service("stripe");
|
|
439
|
+
const stripe = await stripeService2.initializeStripe();
|
|
440
|
+
if (!stripe) {
|
|
441
|
+
return ctx.serviceUnavailable("Stripe service not initialized");
|
|
442
|
+
}
|
|
443
|
+
const paymentIntent = await stripe.paymentIntents.retrieve(paymentIntentId);
|
|
444
|
+
const strapiPayments = await strapi.documents("plugin::payment-plugin.payment").findMany({
|
|
445
|
+
filters: {
|
|
446
|
+
stripe_payment_intent_id: paymentIntentId
|
|
447
|
+
},
|
|
448
|
+
populate: ["customer", "order"]
|
|
449
|
+
});
|
|
450
|
+
const strapiPayment = strapiPayments.length > 0 ? strapiPayments[0] : null;
|
|
402
451
|
ctx.body = {
|
|
403
452
|
success: true,
|
|
404
|
-
|
|
405
|
-
|
|
453
|
+
data: {
|
|
454
|
+
stripe: {
|
|
455
|
+
id: paymentIntent.id,
|
|
456
|
+
amount: paymentIntent.amount,
|
|
457
|
+
currency: paymentIntent.currency,
|
|
458
|
+
status: paymentIntent.status,
|
|
459
|
+
created: paymentIntent.created,
|
|
460
|
+
metadata: paymentIntent.metadata
|
|
461
|
+
},
|
|
462
|
+
strapi: strapiPayment
|
|
463
|
+
}
|
|
406
464
|
};
|
|
407
465
|
} catch (error) {
|
|
408
|
-
|
|
409
|
-
ctx.
|
|
410
|
-
success: false,
|
|
411
|
-
message: error.message
|
|
412
|
-
};
|
|
413
|
-
ctx.status = 400;
|
|
466
|
+
strapi.log.error("Failed to get payment details", { error });
|
|
467
|
+
ctx.internalServerError("Failed to get payment details");
|
|
414
468
|
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
469
|
+
},
|
|
470
|
+
/**
|
|
471
|
+
* List payments with filtering and pagination
|
|
472
|
+
*/
|
|
473
|
+
async listPayments(ctx) {
|
|
419
474
|
try {
|
|
420
|
-
const {
|
|
421
|
-
const
|
|
422
|
-
|
|
475
|
+
const { page = 1, pageSize = 25, status, customer: customer2, order: order2 } = ctx.query;
|
|
476
|
+
const { id: userId } = ctx.state.user;
|
|
477
|
+
const filters = {};
|
|
478
|
+
if (status) filters.status = { $eq: status };
|
|
479
|
+
if (customer2) filters.customer = { $eq: customer2 };
|
|
480
|
+
if (order2) filters.order = { $eq: order2 };
|
|
481
|
+
const isAdmin = ctx.state.user.role?.name === "Administrator";
|
|
482
|
+
if (!isAdmin) {
|
|
483
|
+
filters.$or = [
|
|
484
|
+
{ customer: { $eq: userId } },
|
|
485
|
+
{ order: { $eq: userId } }
|
|
486
|
+
];
|
|
487
|
+
}
|
|
488
|
+
const pageNum = parseInt(page);
|
|
489
|
+
const pageSizeNum = parseInt(pageSize);
|
|
490
|
+
const start = (pageNum - 1) * pageSizeNum;
|
|
491
|
+
const [payments, total] = await Promise.all([
|
|
492
|
+
strapi.documents("plugin::payment-plugin.payment").findMany({
|
|
493
|
+
filters,
|
|
494
|
+
populate: ["customer", "order"],
|
|
495
|
+
sort: { createdAt: "desc" },
|
|
496
|
+
start,
|
|
497
|
+
limit: pageSizeNum
|
|
498
|
+
}),
|
|
499
|
+
strapi.documents("plugin::payment-plugin.payment").count({ filters })
|
|
500
|
+
]);
|
|
501
|
+
ctx.body = {
|
|
502
|
+
success: true,
|
|
503
|
+
data: payments,
|
|
504
|
+
meta: {
|
|
505
|
+
pagination: {
|
|
506
|
+
page: pageNum,
|
|
507
|
+
pageSize: pageSizeNum,
|
|
508
|
+
pageCount: Math.ceil(total / pageSizeNum),
|
|
509
|
+
total
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
};
|
|
423
513
|
} catch (error) {
|
|
424
|
-
|
|
514
|
+
strapi.log.error("Failed to list payments", { error });
|
|
515
|
+
ctx.internalServerError("Failed to list payments");
|
|
425
516
|
}
|
|
426
517
|
},
|
|
427
|
-
|
|
518
|
+
/**
|
|
519
|
+
* List customers with filtering and pagination
|
|
520
|
+
*/
|
|
521
|
+
async listCustomers(ctx) {
|
|
428
522
|
try {
|
|
429
|
-
const {
|
|
430
|
-
const {
|
|
431
|
-
const
|
|
432
|
-
|
|
523
|
+
const { page = 1, pageSize = 25, email } = ctx.query;
|
|
524
|
+
const { id: userId } = ctx.state.user;
|
|
525
|
+
const filters = {};
|
|
526
|
+
if (email) filters.email = { $containsi: email };
|
|
527
|
+
const isAdmin = ctx.state.user.role?.name === "Administrator";
|
|
528
|
+
if (!isAdmin) {
|
|
529
|
+
filters.documentId = userId;
|
|
530
|
+
}
|
|
531
|
+
const pageNum = parseInt(page);
|
|
532
|
+
const pageSizeNum = parseInt(pageSize);
|
|
533
|
+
const start = (pageNum - 1) * pageSizeNum;
|
|
534
|
+
const [customers, total] = await Promise.all([
|
|
535
|
+
strapi.documents("plugin::payment-plugin.customer").findMany({
|
|
536
|
+
filters,
|
|
537
|
+
sort: { createdAt: "desc" },
|
|
538
|
+
start,
|
|
539
|
+
limit: pageSizeNum
|
|
540
|
+
}),
|
|
541
|
+
strapi.documents("plugin::payment-plugin.customer").count({ filters })
|
|
542
|
+
]);
|
|
543
|
+
ctx.body = {
|
|
544
|
+
success: true,
|
|
545
|
+
data: customers,
|
|
546
|
+
meta: {
|
|
547
|
+
pagination: {
|
|
548
|
+
page: pageNum,
|
|
549
|
+
pageSize: pageSizeNum,
|
|
550
|
+
pageCount: Math.ceil(total / pageSizeNum),
|
|
551
|
+
total
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
};
|
|
433
555
|
} catch (error) {
|
|
434
|
-
|
|
556
|
+
strapi.log.error("Failed to list customers", { error });
|
|
557
|
+
ctx.internalServerError("Failed to list customers");
|
|
435
558
|
}
|
|
436
559
|
},
|
|
437
|
-
|
|
560
|
+
/**
|
|
561
|
+
* Create a new order
|
|
562
|
+
*/
|
|
563
|
+
async createOrder(ctx) {
|
|
438
564
|
try {
|
|
439
|
-
const
|
|
440
|
-
|
|
565
|
+
const { orderNumber, totalAmount, currency, items, customerId, metadata = {} } = ctx.request.body;
|
|
566
|
+
if (!orderNumber || !totalAmount || !currency || !items) {
|
|
567
|
+
return ctx.badRequest("Order number, total amount, currency, and items are required");
|
|
568
|
+
}
|
|
569
|
+
if (totalAmount <= 0) {
|
|
570
|
+
return ctx.badRequest("Total amount must be greater than 0");
|
|
571
|
+
}
|
|
572
|
+
if (customerId) {
|
|
573
|
+
const customers = await strapi.documents("plugin::payment-plugin.customer").findMany({
|
|
574
|
+
filters: {
|
|
575
|
+
documentId: customerId
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
if (customers.length === 0) {
|
|
579
|
+
return ctx.badRequest("Customer not found");
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
const orderData = {
|
|
583
|
+
order_number: orderNumber,
|
|
584
|
+
total_amount: totalAmount,
|
|
585
|
+
currency,
|
|
586
|
+
status: "pending",
|
|
587
|
+
customer: customerId,
|
|
588
|
+
items,
|
|
589
|
+
metadata
|
|
590
|
+
};
|
|
591
|
+
const order2 = await strapi.documents("plugin::payment-plugin.order").create({
|
|
592
|
+
data: orderData
|
|
441
593
|
});
|
|
442
|
-
ctx.body =
|
|
594
|
+
ctx.body = {
|
|
595
|
+
success: true,
|
|
596
|
+
data: {
|
|
597
|
+
orderId: order2.documentId,
|
|
598
|
+
orderNumber: order2.order_number,
|
|
599
|
+
status: order2.status,
|
|
600
|
+
totalAmount: order2.total_amount,
|
|
601
|
+
currency: order2.currency,
|
|
602
|
+
created: order2.createdAt
|
|
603
|
+
}
|
|
604
|
+
};
|
|
443
605
|
} catch (error) {
|
|
444
|
-
|
|
606
|
+
strapi.log.error("Failed to create order", { error });
|
|
607
|
+
ctx.internalServerError("Failed to create order");
|
|
445
608
|
}
|
|
446
609
|
},
|
|
447
|
-
|
|
610
|
+
/**
|
|
611
|
+
* Get order details by ID
|
|
612
|
+
*/
|
|
613
|
+
async getOrderDetails(ctx) {
|
|
448
614
|
try {
|
|
449
615
|
const { id } = ctx.params;
|
|
450
|
-
const
|
|
451
|
-
|
|
616
|
+
const { id: userId } = ctx.state.user;
|
|
617
|
+
if (!id) {
|
|
618
|
+
return ctx.badRequest("Order ID is required");
|
|
619
|
+
}
|
|
620
|
+
const isAdmin = ctx.state.user.role?.name === "Administrator";
|
|
621
|
+
const filters = { documentId: id };
|
|
622
|
+
if (!isAdmin) {
|
|
623
|
+
filters.$or = [
|
|
624
|
+
{ customer: { $eq: userId } },
|
|
625
|
+
{ customer: { $eq: userId } }
|
|
626
|
+
];
|
|
627
|
+
}
|
|
628
|
+
const orders = await strapi.documents("plugin::payment-plugin.order").findMany({
|
|
629
|
+
filters,
|
|
630
|
+
populate: ["customer"]
|
|
452
631
|
});
|
|
453
|
-
if (
|
|
454
|
-
return ctx.notFound("
|
|
632
|
+
if (orders.length === 0) {
|
|
633
|
+
return ctx.notFound("Order not found");
|
|
455
634
|
}
|
|
456
|
-
|
|
635
|
+
const order2 = orders[0];
|
|
636
|
+
const payments = await strapi.documents("plugin::payment-plugin.payment").findMany({
|
|
637
|
+
filters: { order: { $eq: order2.documentId } },
|
|
638
|
+
sort: { createdAt: "desc" }
|
|
639
|
+
});
|
|
640
|
+
ctx.body = {
|
|
641
|
+
success: true,
|
|
642
|
+
data: {
|
|
643
|
+
order: order2,
|
|
644
|
+
payments
|
|
645
|
+
}
|
|
646
|
+
};
|
|
457
647
|
} catch (error) {
|
|
458
|
-
|
|
648
|
+
strapi.log.error("Failed to get order details", { error });
|
|
649
|
+
ctx.internalServerError("Failed to get order details");
|
|
459
650
|
}
|
|
460
651
|
},
|
|
461
|
-
|
|
652
|
+
/**
|
|
653
|
+
* List orders with filtering and pagination
|
|
654
|
+
*/
|
|
655
|
+
async listOrders(ctx) {
|
|
462
656
|
try {
|
|
463
|
-
const {
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
}
|
|
467
|
-
if (
|
|
468
|
-
|
|
657
|
+
const { page = 1, pageSize = 25, status, customer: customer2 } = ctx.query;
|
|
658
|
+
const { id: userId } = ctx.state.user;
|
|
659
|
+
const filters = {};
|
|
660
|
+
if (status) filters.status = { $eq: status };
|
|
661
|
+
if (customer2) filters.customer = { $eq: customer2 };
|
|
662
|
+
const isAdmin = ctx.state.user.role?.name === "Administrator";
|
|
663
|
+
if (!isAdmin) {
|
|
664
|
+
filters.customer = { $eq: userId };
|
|
469
665
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
666
|
+
const pageNum = parseInt(page);
|
|
667
|
+
const pageSizeNum = parseInt(pageSize);
|
|
668
|
+
const start = (pageNum - 1) * pageSizeNum;
|
|
669
|
+
const [orders, total] = await Promise.all([
|
|
670
|
+
strapi.documents("plugin::payment-plugin.order").findMany({
|
|
671
|
+
filters,
|
|
672
|
+
populate: ["customer"],
|
|
673
|
+
sort: { createdAt: "desc" },
|
|
674
|
+
start,
|
|
675
|
+
limit: pageSizeNum
|
|
676
|
+
}),
|
|
677
|
+
strapi.documents("plugin::payment-plugin.order").count({ filters })
|
|
678
|
+
]);
|
|
679
|
+
ctx.body = {
|
|
680
|
+
success: true,
|
|
681
|
+
data: orders,
|
|
682
|
+
meta: {
|
|
683
|
+
pagination: {
|
|
684
|
+
page: pageNum,
|
|
685
|
+
pageSize: pageSizeNum,
|
|
686
|
+
pageCount: Math.ceil(total / pageSizeNum),
|
|
687
|
+
total
|
|
688
|
+
}
|
|
475
689
|
}
|
|
476
|
-
}
|
|
477
|
-
await strapi.db.query("plugin::strapi-payment-plugin.product").delete({
|
|
478
|
-
where: { id }
|
|
479
|
-
});
|
|
480
|
-
ctx.body = { message: "Product deleted successfully" };
|
|
690
|
+
};
|
|
481
691
|
} catch (error) {
|
|
482
|
-
|
|
692
|
+
strapi.log.error("Failed to list orders", { error });
|
|
693
|
+
ctx.internalServerError("Failed to list orders");
|
|
483
694
|
}
|
|
484
695
|
},
|
|
485
|
-
|
|
696
|
+
// Admin Controller Methods
|
|
697
|
+
/**
|
|
698
|
+
* Get dashboard data for admin panel
|
|
699
|
+
*/
|
|
700
|
+
async getDashboardData(ctx) {
|
|
486
701
|
try {
|
|
487
|
-
const {
|
|
488
|
-
const
|
|
489
|
-
|
|
702
|
+
const { period = "30d" } = ctx.query;
|
|
703
|
+
const now = /* @__PURE__ */ new Date();
|
|
704
|
+
const days = parseInt(period);
|
|
705
|
+
const startDate = new Date(now.getTime() - days * 24 * 60 * 60 * 1e3);
|
|
706
|
+
const [totalPayments, successfulPayments, failedPayments, totalRevenue] = await Promise.all([
|
|
707
|
+
strapi.documents("plugin::payment-plugin.payment").count({}),
|
|
708
|
+
strapi.documents("plugin::payment-plugin.payment").count({ filters: { status: { $eq: "succeeded" } } }),
|
|
709
|
+
strapi.documents("plugin::payment-plugin.payment").count({ filters: { status: { $eq: "failed" } } }),
|
|
710
|
+
strapi.documents("plugin::payment-plugin.payment").findMany({
|
|
711
|
+
filters: {
|
|
712
|
+
status: { $eq: "succeeded" },
|
|
713
|
+
createdAt: { $gte: startDate.toISOString() }
|
|
714
|
+
},
|
|
715
|
+
fields: ["amount"]
|
|
716
|
+
})
|
|
717
|
+
]);
|
|
718
|
+
const revenue = totalRevenue.reduce((sum, payment2) => sum + payment2.amount, 0);
|
|
719
|
+
const recentTransactions = await strapi.documents("plugin::payment-plugin.payment").findMany({
|
|
720
|
+
filters: { createdAt: { $gte: startDate.toISOString() } },
|
|
721
|
+
populate: ["customer", "order"],
|
|
722
|
+
sort: { createdAt: "desc" },
|
|
723
|
+
limit: 10
|
|
490
724
|
});
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
725
|
+
const topCustomers = await strapi.documents("plugin::payment-plugin.customer").findMany({
|
|
726
|
+
sort: { createdAt: "desc" },
|
|
727
|
+
limit: 5
|
|
728
|
+
});
|
|
729
|
+
ctx.body = {
|
|
730
|
+
success: true,
|
|
731
|
+
data: {
|
|
732
|
+
summary: {
|
|
733
|
+
totalPayments,
|
|
734
|
+
successfulPayments,
|
|
735
|
+
failedPayments,
|
|
736
|
+
successRate: totalPayments > 0 ? successfulPayments / totalPayments * 100 : 0,
|
|
737
|
+
totalRevenue: revenue,
|
|
738
|
+
period
|
|
739
|
+
},
|
|
740
|
+
recentTransactions,
|
|
741
|
+
topCustomers
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
} catch (error) {
|
|
745
|
+
strapi.log.error("Failed to get dashboard data", { error });
|
|
746
|
+
ctx.internalServerError("Failed to get dashboard data");
|
|
747
|
+
}
|
|
748
|
+
},
|
|
749
|
+
/**
|
|
750
|
+
* Get payment reports
|
|
751
|
+
*/
|
|
752
|
+
async getPaymentReports(ctx) {
|
|
753
|
+
try {
|
|
754
|
+
const { startDate, endDate, format = "json" } = ctx.query;
|
|
755
|
+
const filters = {};
|
|
756
|
+
if (startDate || endDate) {
|
|
757
|
+
filters.createdAt = {};
|
|
758
|
+
if (startDate) filters.createdAt.$gte = new Date(startDate).toISOString();
|
|
759
|
+
if (endDate) filters.createdAt.$lte = new Date(endDate).toISOString();
|
|
507
760
|
}
|
|
761
|
+
const payments = await strapi.documents("plugin::payment-plugin.payment").findMany({
|
|
762
|
+
filters,
|
|
763
|
+
populate: ["customer", "order"],
|
|
764
|
+
sort: { createdAt: "desc" }
|
|
765
|
+
});
|
|
766
|
+
const summary = {
|
|
767
|
+
totalPayments: payments.length,
|
|
768
|
+
totalAmount: payments.reduce((sum, p) => sum + p.amount, 0),
|
|
769
|
+
byStatus: payments.reduce((acc, p) => {
|
|
770
|
+
acc[p.status] = (acc[p.status] || 0) + 1;
|
|
771
|
+
return acc;
|
|
772
|
+
}, {}),
|
|
773
|
+
byCurrency: payments.reduce((acc, p) => {
|
|
774
|
+
acc[p.currency] = (acc[p.currency] || 0) + 1;
|
|
775
|
+
return acc;
|
|
776
|
+
}, {})
|
|
777
|
+
};
|
|
778
|
+
ctx.body = {
|
|
779
|
+
success: true,
|
|
780
|
+
data: {
|
|
781
|
+
summary,
|
|
782
|
+
payments,
|
|
783
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
784
|
+
dateRange: { startDate, endDate }
|
|
785
|
+
}
|
|
786
|
+
};
|
|
508
787
|
} catch (error) {
|
|
509
|
-
|
|
788
|
+
strapi.log.error("Failed to get payment reports", { error });
|
|
789
|
+
ctx.internalServerError("Failed to get payment reports");
|
|
510
790
|
}
|
|
511
791
|
},
|
|
512
|
-
|
|
792
|
+
/**
|
|
793
|
+
* Get plugin configuration
|
|
794
|
+
*/
|
|
795
|
+
async getPluginConfig(ctx) {
|
|
513
796
|
try {
|
|
514
|
-
const
|
|
515
|
-
|
|
797
|
+
const store = strapi.store({ type: "plugin", name: "payment-plugin" });
|
|
798
|
+
const config2 = await store.get({ key: "settings" }) || {};
|
|
799
|
+
const staticConfig = strapi.config.get("plugin::payment-plugin") || {};
|
|
800
|
+
const mergedConfig = { ...staticConfig || {}, ...config2 || {} };
|
|
801
|
+
const sanitizedConfig = {
|
|
802
|
+
...mergedConfig,
|
|
803
|
+
stripe: mergedConfig.stripe ? {
|
|
804
|
+
...mergedConfig.stripe,
|
|
805
|
+
publishableKey: mergedConfig.stripe.publishableKey || process.env.STRIPE_PUBLISHABLE_KEY || (process.env.STRIPE_API_KEY?.startsWith("pk_") ? process.env.STRIPE_API_KEY : ""),
|
|
806
|
+
secretKey: mergedConfig.stripe.secretKey || process.env.STRIPE_SECRET_KEY || process.env.STRIPE_WEBHOOK_SECRET?.startsWith("sk_") ? "***configured***" : null,
|
|
807
|
+
webhookSecret: mergedConfig.stripe.webhookSecret || process.env.STRIPE_WEBHOOK_SECRET?.startsWith("whsec_") ? "***configured***" : null
|
|
808
|
+
} : {}
|
|
809
|
+
};
|
|
810
|
+
ctx.body = {
|
|
811
|
+
success: true,
|
|
812
|
+
data: sanitizedConfig
|
|
813
|
+
};
|
|
516
814
|
} catch (error) {
|
|
517
|
-
|
|
815
|
+
strapi.log.error("Failed to get plugin config", { error });
|
|
816
|
+
ctx.internalServerError("Failed to get plugin config");
|
|
518
817
|
}
|
|
519
818
|
},
|
|
520
|
-
|
|
819
|
+
/**
|
|
820
|
+
* Update plugin configuration
|
|
821
|
+
*/
|
|
822
|
+
async updatePluginConfig(ctx) {
|
|
521
823
|
try {
|
|
522
|
-
const {
|
|
523
|
-
const
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
824
|
+
const { stripe: newStripe, payment: payment2, logging } = ctx.request.body;
|
|
825
|
+
const store = strapi.store({ type: "plugin", name: "payment-plugin" });
|
|
826
|
+
const currentConfig = await store.get({ key: "settings" }) || {};
|
|
827
|
+
const currentStripe = currentConfig.stripe || {};
|
|
828
|
+
const stripe = {
|
|
829
|
+
...newStripe,
|
|
830
|
+
secretKey: newStripe.secretKey === "***configured***" ? currentStripe.secretKey : newStripe.secretKey,
|
|
831
|
+
webhookSecret: newStripe.webhookSecret === "***configured***" ? currentStripe.webhookSecret : newStripe.webhookSecret
|
|
832
|
+
};
|
|
833
|
+
const config2 = {
|
|
834
|
+
stripe: stripe || {},
|
|
835
|
+
payment: payment2 || {},
|
|
836
|
+
logging: logging || {}
|
|
837
|
+
};
|
|
838
|
+
await store.set({ key: "settings", value: config2 });
|
|
839
|
+
ctx.body = {
|
|
840
|
+
success: true,
|
|
841
|
+
data: { message: "Configuration updated successfully" }
|
|
842
|
+
};
|
|
534
843
|
} catch (error) {
|
|
535
|
-
|
|
844
|
+
strapi.log.error("Failed to update plugin config", { error });
|
|
845
|
+
ctx.internalServerError("Failed to update plugin config");
|
|
536
846
|
}
|
|
537
847
|
},
|
|
538
|
-
|
|
848
|
+
/**
|
|
849
|
+
* Get analytics data
|
|
850
|
+
*/
|
|
851
|
+
async getAnalytics(ctx) {
|
|
539
852
|
try {
|
|
540
|
-
const {
|
|
541
|
-
const
|
|
542
|
-
|
|
853
|
+
const { period = "30d" } = ctx.query;
|
|
854
|
+
const days = parseInt(period);
|
|
855
|
+
const now = /* @__PURE__ */ new Date();
|
|
856
|
+
const startDate = new Date(now.getTime() - days * 24 * 60 * 60 * 1e3);
|
|
857
|
+
const payments = await strapi.documents("plugin::payment-plugin.payment").findMany({
|
|
858
|
+
filters: {
|
|
859
|
+
createdAt: { $gte: startDate.toISOString() },
|
|
860
|
+
status: { $eq: "succeeded" }
|
|
861
|
+
},
|
|
862
|
+
fields: ["amount", "createdAt", "currency"],
|
|
863
|
+
sort: { createdAt: "asc" }
|
|
543
864
|
});
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
return ctx.badRequest("Product is not synced with Stripe");
|
|
549
|
-
}
|
|
550
|
-
const paymentLink = await stripeDriver.createPaymentLink(product2.stripeProductId, {
|
|
551
|
-
metadata: {
|
|
552
|
-
product_id: product2.id
|
|
865
|
+
const dailyStats = payments.reduce((acc, payment2) => {
|
|
866
|
+
const date = new Date(payment2.createdAt).toISOString().split("T")[0];
|
|
867
|
+
if (!acc[date]) {
|
|
868
|
+
acc[date] = { count: 0, amount: 0, currency: payment2.currency };
|
|
553
869
|
}
|
|
554
|
-
|
|
555
|
-
|
|
870
|
+
acc[date].count += 1;
|
|
871
|
+
acc[date].amount += payment2.amount;
|
|
872
|
+
return acc;
|
|
873
|
+
}, {});
|
|
874
|
+
ctx.body = {
|
|
875
|
+
success: true,
|
|
876
|
+
data: {
|
|
877
|
+
period,
|
|
878
|
+
dailyStats,
|
|
879
|
+
totalVolume: payments.reduce((sum, p) => sum + p.amount, 0),
|
|
880
|
+
totalTransactions: payments.length,
|
|
881
|
+
averageTransaction: payments.length > 0 ? payments.reduce((sum, p) => sum + p.amount, 0) / payments.length : 0
|
|
882
|
+
}
|
|
883
|
+
};
|
|
556
884
|
} catch (error) {
|
|
557
|
-
|
|
885
|
+
strapi.log.error("Failed to get analytics", { error });
|
|
886
|
+
ctx.internalServerError("Failed to get analytics");
|
|
558
887
|
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
(
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
ctx.
|
|
569
|
-
} catch (error) {
|
|
570
|
-
ctx.throw(500, error);
|
|
571
|
-
}
|
|
572
|
-
},
|
|
573
|
-
async update(ctx) {
|
|
574
|
-
try {
|
|
575
|
-
const { id } = ctx.params;
|
|
576
|
-
const { data } = ctx.request.body;
|
|
577
|
-
const subscription2 = await strapi.service("plugin::strapi-payment-plugin.subscription").updateSubscription(id, data);
|
|
578
|
-
ctx.body = subscription2;
|
|
579
|
-
} catch (error) {
|
|
580
|
-
ctx.throw(500, error);
|
|
581
|
-
}
|
|
582
|
-
},
|
|
583
|
-
async find(ctx) {
|
|
584
|
-
try {
|
|
585
|
-
console.log("Fetching subscriptions...");
|
|
586
|
-
const subscriptions = await strapi.db.query("plugin::strapi-payment-plugin.subscription").findMany({
|
|
587
|
-
orderBy: { startDate: "desc" },
|
|
588
|
-
populate: ["product", "user"]
|
|
589
|
-
});
|
|
590
|
-
ctx.body = subscriptions;
|
|
591
|
-
} catch (error) {
|
|
592
|
-
ctx.throw(500, error);
|
|
888
|
+
},
|
|
889
|
+
/**
|
|
890
|
+
* Admin create refund
|
|
891
|
+
*/
|
|
892
|
+
async adminCreateRefund(ctx) {
|
|
893
|
+
try {
|
|
894
|
+
const { id: paymentId } = ctx.params;
|
|
895
|
+
const { amount, reason, metadata = {} } = ctx.request.body;
|
|
896
|
+
if (!paymentId) {
|
|
897
|
+
return ctx.badRequest("Payment ID is required");
|
|
593
898
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
where: { id },
|
|
600
|
-
populate: ["product", "user"]
|
|
601
|
-
});
|
|
602
|
-
if (!subscription2) {
|
|
603
|
-
return ctx.notFound("Subscription not found");
|
|
604
|
-
}
|
|
605
|
-
ctx.body = subscription2;
|
|
606
|
-
} catch (error) {
|
|
607
|
-
ctx.throw(500, error);
|
|
899
|
+
const payments = await strapi.documents("plugin::payment-plugin.payment").findMany({
|
|
900
|
+
filters: { documentId: paymentId }
|
|
901
|
+
});
|
|
902
|
+
if (payments.length === 0) {
|
|
903
|
+
return ctx.notFound("Payment not found");
|
|
608
904
|
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
|
|
905
|
+
const payment2 = payments[0];
|
|
906
|
+
const stripeService2 = strapi.plugin("payment-plugin").service("stripe");
|
|
907
|
+
const refund = await stripeService2.createRefund({
|
|
908
|
+
paymentIntentId: payment2.stripe_payment_intent_id,
|
|
909
|
+
amount,
|
|
910
|
+
reason,
|
|
911
|
+
metadata: { ...metadata, admin_refund: true }
|
|
912
|
+
});
|
|
913
|
+
await strapi.documents("plugin::payment-plugin.payment").update({
|
|
914
|
+
documentId: payment2.documentId,
|
|
915
|
+
data: { status: "refunded" }
|
|
916
|
+
});
|
|
917
|
+
ctx.body = {
|
|
918
|
+
success: true,
|
|
919
|
+
data: {
|
|
920
|
+
refundId: refund.id,
|
|
921
|
+
amount: refund.amount,
|
|
922
|
+
currency: refund.currency,
|
|
923
|
+
status: refund.status,
|
|
924
|
+
reason: refund.reason
|
|
618
925
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
926
|
+
};
|
|
927
|
+
} catch (error) {
|
|
928
|
+
strapi.log.error("Failed to create admin refund", { error });
|
|
929
|
+
ctx.internalServerError("Failed to create admin refund");
|
|
930
|
+
}
|
|
931
|
+
},
|
|
932
|
+
/**
|
|
933
|
+
* Admin list payments (no user restrictions)
|
|
934
|
+
*/
|
|
935
|
+
async adminListPayments(ctx) {
|
|
936
|
+
try {
|
|
937
|
+
const { page = 1, pageSize = 25, status, customer: customer2, order: order2 } = ctx.query;
|
|
938
|
+
const filters = {};
|
|
939
|
+
if (status) filters.status = { $eq: status };
|
|
940
|
+
if (customer2) filters.customer = { $eq: customer2 };
|
|
941
|
+
if (order2) filters.order = { $eq: order2 };
|
|
942
|
+
const pageNum = parseInt(page);
|
|
943
|
+
const pageSizeNum = parseInt(pageSize);
|
|
944
|
+
const start = (pageNum - 1) * pageSizeNum;
|
|
945
|
+
const [payments, total] = await Promise.all([
|
|
946
|
+
strapi.documents("plugin::payment-plugin.payment").findMany({
|
|
947
|
+
filters,
|
|
948
|
+
populate: ["customer", "order"],
|
|
949
|
+
sort: { createdAt: "desc" },
|
|
950
|
+
start,
|
|
951
|
+
limit: pageSizeNum
|
|
952
|
+
}),
|
|
953
|
+
strapi.documents("plugin::payment-plugin.payment").count({ filters })
|
|
954
|
+
]);
|
|
955
|
+
ctx.body = {
|
|
956
|
+
success: true,
|
|
957
|
+
data: payments,
|
|
958
|
+
meta: {
|
|
959
|
+
pagination: {
|
|
960
|
+
page: pageNum,
|
|
961
|
+
pageSize: pageSizeNum,
|
|
962
|
+
pageCount: Math.ceil(total / pageSizeNum),
|
|
963
|
+
total
|
|
625
964
|
}
|
|
626
965
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
966
|
+
};
|
|
967
|
+
} catch (error) {
|
|
968
|
+
strapi.log.error("Failed to list admin payments", { error });
|
|
969
|
+
ctx.internalServerError("Failed to list admin payments");
|
|
970
|
+
}
|
|
971
|
+
},
|
|
972
|
+
/**
|
|
973
|
+
* Admin list customers (no user restrictions)
|
|
974
|
+
*/
|
|
975
|
+
async adminListCustomers(ctx) {
|
|
976
|
+
try {
|
|
977
|
+
const { page = 1, pageSize = 25, email } = ctx.query;
|
|
978
|
+
const filters = {};
|
|
979
|
+
if (email) filters.email = { $containsi: email };
|
|
980
|
+
const pageNum = parseInt(page);
|
|
981
|
+
const pageSizeNum = parseInt(pageSize);
|
|
982
|
+
const start = (pageNum - 1) * pageSizeNum;
|
|
983
|
+
const [customers, total] = await Promise.all([
|
|
984
|
+
strapi.documents("plugin::payment-plugin.customer").findMany({
|
|
985
|
+
filters,
|
|
986
|
+
populate: {
|
|
987
|
+
payments: {
|
|
988
|
+
filters: { status: { $eq: "succeeded" } },
|
|
989
|
+
fields: ["amount"]
|
|
990
|
+
}
|
|
991
|
+
},
|
|
992
|
+
sort: { createdAt: "desc" },
|
|
993
|
+
start,
|
|
994
|
+
limit: pageSizeNum
|
|
995
|
+
}),
|
|
996
|
+
strapi.documents("plugin::payment-plugin.customer").count({ filters })
|
|
997
|
+
]);
|
|
998
|
+
const customersWithTotals = customers.map((customer2) => {
|
|
999
|
+
const successfulPayments = customer2.payments || [];
|
|
1000
|
+
const totalSpent = successfulPayments.reduce((sum, p) => sum + p.amount, 0);
|
|
1001
|
+
const paymentCount = successfulPayments.length;
|
|
1002
|
+
return {
|
|
1003
|
+
...customer2,
|
|
1004
|
+
totalSpent,
|
|
1005
|
+
paymentCount
|
|
1006
|
+
};
|
|
1007
|
+
});
|
|
1008
|
+
ctx.body = {
|
|
1009
|
+
success: true,
|
|
1010
|
+
data: customersWithTotals,
|
|
1011
|
+
meta: {
|
|
1012
|
+
pagination: {
|
|
1013
|
+
page: pageNum,
|
|
1014
|
+
pageSize: pageSizeNum,
|
|
1015
|
+
pageCount: Math.ceil(total / pageSizeNum),
|
|
1016
|
+
total
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
};
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
strapi.log.error("Failed to list admin customers", { error });
|
|
1022
|
+
ctx.internalServerError("Failed to list admin customers");
|
|
1023
|
+
}
|
|
1024
|
+
},
|
|
1025
|
+
/**
|
|
1026
|
+
* Admin list orders (no user restrictions)
|
|
1027
|
+
*/
|
|
1028
|
+
async adminListOrders(ctx) {
|
|
1029
|
+
try {
|
|
1030
|
+
const { page = 1, pageSize = 25, status, customer: customer2 } = ctx.query;
|
|
1031
|
+
const filters = {};
|
|
1032
|
+
if (status) filters.status = { $eq: status };
|
|
1033
|
+
if (customer2) filters.customer = { $eq: customer2 };
|
|
1034
|
+
const pageNum = parseInt(page);
|
|
1035
|
+
const pageSizeNum = parseInt(pageSize);
|
|
1036
|
+
const start = (pageNum - 1) * pageSizeNum;
|
|
1037
|
+
const [orders, total] = await Promise.all([
|
|
1038
|
+
strapi.documents("plugin::payment-plugin.order").findMany({
|
|
1039
|
+
filters,
|
|
1040
|
+
populate: ["customer"],
|
|
1041
|
+
sort: { createdAt: "desc" },
|
|
1042
|
+
start,
|
|
1043
|
+
limit: pageSizeNum
|
|
1044
|
+
}),
|
|
1045
|
+
strapi.documents("plugin::payment-plugin.order").count({ filters })
|
|
1046
|
+
]);
|
|
1047
|
+
ctx.body = {
|
|
1048
|
+
success: true,
|
|
1049
|
+
data: orders,
|
|
1050
|
+
meta: {
|
|
1051
|
+
pagination: {
|
|
1052
|
+
page: pageNum,
|
|
1053
|
+
pageSize: pageSizeNum,
|
|
1054
|
+
pageCount: Math.ceil(total / pageSizeNum),
|
|
1055
|
+
total
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
};
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
strapi.log.error("Failed to list admin orders", { error });
|
|
1061
|
+
ctx.internalServerError("Failed to list admin orders");
|
|
1062
|
+
}
|
|
1063
|
+
},
|
|
1064
|
+
/**
|
|
1065
|
+
* Test webhook endpoint
|
|
1066
|
+
*/
|
|
1067
|
+
async testWebhook(ctx) {
|
|
1068
|
+
try {
|
|
1069
|
+
const { eventType = "payment_intent.succeeded" } = ctx.request.body;
|
|
1070
|
+
const stripeService2 = strapi.plugin("payment-plugin").service("stripe");
|
|
1071
|
+
const testEvent = {
|
|
1072
|
+
id: `evt_test_${Date.now()}`,
|
|
1073
|
+
object: "event",
|
|
1074
|
+
type: eventType,
|
|
1075
|
+
data: {
|
|
1076
|
+
object: {
|
|
1077
|
+
id: `pi_test_${Date.now()}`,
|
|
1078
|
+
object: "payment_intent",
|
|
1079
|
+
amount: 1e3,
|
|
1080
|
+
currency: "usd",
|
|
1081
|
+
status: "succeeded",
|
|
1082
|
+
metadata: { test: true }
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
};
|
|
1086
|
+
await stripeService2.handleWebhook(testEvent);
|
|
1087
|
+
ctx.body = {
|
|
1088
|
+
success: true,
|
|
1089
|
+
data: {
|
|
1090
|
+
message: "Test webhook processed successfully",
|
|
1091
|
+
eventId: testEvent.id,
|
|
1092
|
+
eventType: testEvent.type
|
|
1093
|
+
}
|
|
1094
|
+
};
|
|
1095
|
+
} catch (error) {
|
|
1096
|
+
strapi.log.error("Failed to test webhook", { error });
|
|
1097
|
+
ctx.internalServerError("Failed to test webhook");
|
|
1098
|
+
}
|
|
1099
|
+
},
|
|
1100
|
+
/**
|
|
1101
|
+
* Initialize a functional payment flow (Customer -> Order -> PI)
|
|
1102
|
+
*/
|
|
1103
|
+
async initializePayment(ctx) {
|
|
1104
|
+
try {
|
|
1105
|
+
const stripeService2 = strapi.plugin("payment-plugin").service("stripe");
|
|
1106
|
+
const {
|
|
1107
|
+
amount = 5e3,
|
|
1108
|
+
currency = "usd",
|
|
1109
|
+
email = `customer-${Date.now()}@example.com`,
|
|
1110
|
+
firstName = "John",
|
|
1111
|
+
lastName = "Doe",
|
|
1112
|
+
confirm = false,
|
|
1113
|
+
payment_method
|
|
1114
|
+
} = ctx.request.body;
|
|
1115
|
+
const flowResult = await stripeService2.initializePaymentFlow({
|
|
1116
|
+
amount,
|
|
1117
|
+
currency,
|
|
1118
|
+
email,
|
|
1119
|
+
firstName,
|
|
1120
|
+
lastName,
|
|
1121
|
+
confirm,
|
|
1122
|
+
payment_method,
|
|
1123
|
+
metadata: { source: "admin_dashboard_init" }
|
|
1124
|
+
});
|
|
1125
|
+
ctx.body = {
|
|
1126
|
+
success: true,
|
|
1127
|
+
data: flowResult
|
|
1128
|
+
};
|
|
1129
|
+
} catch (error) {
|
|
1130
|
+
strapi.log.error("Failed to initialize payment", { error });
|
|
1131
|
+
ctx.internalServerError(error instanceof Error ? error.message : "Failed to initialize payment");
|
|
1132
|
+
}
|
|
1133
|
+
},
|
|
1134
|
+
/**
|
|
1135
|
+
* Create and confirm a sample payment for dashboard demonstration
|
|
1136
|
+
*/
|
|
1137
|
+
async createSamplePayment(ctx) {
|
|
1138
|
+
try {
|
|
1139
|
+
const stripeService2 = strapi.plugin("payment-plugin").service("stripe");
|
|
1140
|
+
const flowResult = await stripeService2.initializePaymentFlow({
|
|
1141
|
+
amount: 2500,
|
|
1142
|
+
currency: "usd",
|
|
1143
|
+
email: `sample@example.com`,
|
|
1144
|
+
firstName: "Sample",
|
|
1145
|
+
lastName: "Customer",
|
|
1146
|
+
confirm: true,
|
|
1147
|
+
payment_method: "pm_card_visa",
|
|
1148
|
+
// Still using this for the 'test' button to show a success
|
|
1149
|
+
metadata: { is_sample: "true" }
|
|
1150
|
+
});
|
|
1151
|
+
if (flowResult.stripePaymentIntent.status === "succeeded") {
|
|
1152
|
+
await stripeService2.updateStrapiOrder(flowResult.order.documentId, {
|
|
1153
|
+
status: "completed"
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
ctx.body = {
|
|
1157
|
+
success: true,
|
|
1158
|
+
data: flowResult
|
|
1159
|
+
};
|
|
1160
|
+
} catch (error) {
|
|
1161
|
+
strapi.log.error("Failed to create sample payment", { error });
|
|
1162
|
+
ctx.internalServerError(error instanceof Error ? error.message : "Failed to create sample payment");
|
|
652
1163
|
}
|
|
653
|
-
}
|
|
654
|
-
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
655
1166
|
const controllers = {
|
|
656
1167
|
controller,
|
|
657
|
-
|
|
658
|
-
subscription: subscription$1
|
|
1168
|
+
stripe: stripeController
|
|
659
1169
|
};
|
|
660
|
-
const middlewares = {
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
1170
|
+
const middlewares = {
|
|
1171
|
+
/**
|
|
1172
|
+
* Webhook signature validator
|
|
1173
|
+
*/
|
|
1174
|
+
"webhook-validator": async (ctx, next) => {
|
|
1175
|
+
try {
|
|
1176
|
+
const signature = ctx.request.headers["stripe-signature"];
|
|
1177
|
+
if (!signature) {
|
|
1178
|
+
return ctx.badRequest("Missing Stripe signature header");
|
|
1179
|
+
}
|
|
1180
|
+
if (!ctx.request.body || typeof ctx.request.body === "object") {
|
|
1181
|
+
strapi.log.warn("Webhook body already parsed - signature verification may fail");
|
|
1182
|
+
}
|
|
1183
|
+
ctx.state.isWebhook = true;
|
|
1184
|
+
ctx.state.webhookSignature = signature;
|
|
1185
|
+
await next();
|
|
1186
|
+
} catch (error) {
|
|
1187
|
+
strapi.log.error("Webhook validation failed", { error });
|
|
1188
|
+
return ctx.badRequest("Invalid webhook signature");
|
|
1189
|
+
}
|
|
1190
|
+
},
|
|
1191
|
+
/**
|
|
1192
|
+
* Rate limiter middleware for sensitive operations
|
|
1193
|
+
*/
|
|
1194
|
+
rateLimiter: async (ctx, next) => {
|
|
1195
|
+
const clientIP = ctx.request.ip || ctx.request.connection?.remoteAddress || "unknown";
|
|
1196
|
+
const now = Date.now();
|
|
1197
|
+
const windowMs = 60 * 1e3;
|
|
1198
|
+
const maxRequests = 10;
|
|
1199
|
+
if (!strapi.plugin("payment-plugin").service("rateLimiter")) {
|
|
1200
|
+
const config2 = strapi.config.get("plugin::payment-plugin") || {};
|
|
1201
|
+
config2.rateLimits = config2.rateLimits || {};
|
|
1202
|
+
await strapi.config.set("plugin::payment-plugin", config2);
|
|
1203
|
+
}
|
|
1204
|
+
const pluginConfig = strapi.config.get("plugin::payment-plugin") || {};
|
|
1205
|
+
const rateLimits = pluginConfig.rateLimits || {};
|
|
1206
|
+
const clientLimits = rateLimits[clientIP] || { count: 0, resetTime: now + windowMs };
|
|
1207
|
+
if (now > clientLimits.resetTime) {
|
|
1208
|
+
clientLimits.count = 0;
|
|
1209
|
+
clientLimits.resetTime = now + windowMs;
|
|
1210
|
+
}
|
|
1211
|
+
if (clientLimits.count >= maxRequests) {
|
|
1212
|
+
strapi.log.warn("Rate limit exceeded", { clientIP, endpoint: ctx.path });
|
|
1213
|
+
return ctx.tooManyRequests("Too many requests. Please try again later.");
|
|
1214
|
+
}
|
|
1215
|
+
clientLimits.count++;
|
|
1216
|
+
rateLimits[clientIP] = clientLimits;
|
|
1217
|
+
pluginConfig.rateLimits = rateLimits;
|
|
1218
|
+
await strapi.config.set("plugin::payment-plugin", pluginConfig);
|
|
1219
|
+
await next();
|
|
1220
|
+
},
|
|
1221
|
+
/**
|
|
1222
|
+
* Input sanitization middleware
|
|
1223
|
+
*/
|
|
1224
|
+
inputSanitizer: async (ctx, next) => {
|
|
1225
|
+
if (ctx.request.body && typeof ctx.request.body === "object") {
|
|
1226
|
+
ctx.request.body = sanitizeObject(ctx.request.body);
|
|
1227
|
+
}
|
|
1228
|
+
if (ctx.query && typeof ctx.query === "object") {
|
|
1229
|
+
ctx.query = sanitizeObject(ctx.query);
|
|
1230
|
+
}
|
|
1231
|
+
await next();
|
|
1232
|
+
}
|
|
1233
|
+
};
|
|
1234
|
+
function sanitizeObject(obj) {
|
|
1235
|
+
if (typeof obj === "string") {
|
|
1236
|
+
return sanitizeString(obj);
|
|
1237
|
+
}
|
|
1238
|
+
if (Array.isArray(obj)) {
|
|
1239
|
+
return obj.map(sanitizeObject);
|
|
1240
|
+
}
|
|
1241
|
+
if (obj && typeof obj === "object") {
|
|
1242
|
+
const sanitized = {};
|
|
1243
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1244
|
+
if (/^[a-zA-Z0-9_.-]+$/.test(key)) {
|
|
1245
|
+
sanitized[key] = sanitizeObject(value);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
return sanitized;
|
|
1249
|
+
}
|
|
1250
|
+
return obj;
|
|
1251
|
+
}
|
|
1252
|
+
function sanitizeString(str) {
|
|
1253
|
+
return str.replace(/[<>\"']/g, "").trim();
|
|
1254
|
+
}
|
|
1255
|
+
const policies = {
|
|
1256
|
+
isAuthenticated: (ctx) => {
|
|
1257
|
+
if (!ctx.state.user) {
|
|
1258
|
+
return false;
|
|
1259
|
+
}
|
|
1260
|
+
return true;
|
|
1261
|
+
}
|
|
1262
|
+
};
|
|
1263
|
+
const routes$2 = {
|
|
1264
|
+
type: "content-api",
|
|
664
1265
|
routes: [
|
|
1266
|
+
// Stripe Webhook Handler
|
|
665
1267
|
{
|
|
666
|
-
method: "
|
|
667
|
-
path: "/
|
|
668
|
-
handler: "
|
|
1268
|
+
method: "POST",
|
|
1269
|
+
path: "/webhook",
|
|
1270
|
+
handler: "stripe.handleWebhook",
|
|
669
1271
|
config: {
|
|
670
|
-
|
|
671
|
-
|
|
1272
|
+
auth: false,
|
|
1273
|
+
policies: []
|
|
1274
|
+
}
|
|
1275
|
+
},
|
|
1276
|
+
// Payment Intent Operations
|
|
1277
|
+
{
|
|
1278
|
+
method: "POST",
|
|
1279
|
+
path: "/payments/create-intent",
|
|
1280
|
+
handler: "stripe.createPaymentIntent",
|
|
1281
|
+
config: {
|
|
1282
|
+
policies: []
|
|
1283
|
+
}
|
|
1284
|
+
},
|
|
1285
|
+
{
|
|
1286
|
+
method: "POST",
|
|
1287
|
+
path: "/payments/confirm/:id",
|
|
1288
|
+
handler: "stripe.confirmPayment",
|
|
1289
|
+
config: {
|
|
1290
|
+
policies: []
|
|
672
1291
|
}
|
|
673
1292
|
},
|
|
674
1293
|
{
|
|
675
1294
|
method: "GET",
|
|
676
|
-
path: "/
|
|
677
|
-
handler: "
|
|
1295
|
+
path: "/payments/:id",
|
|
1296
|
+
handler: "stripe.getPaymentDetails",
|
|
678
1297
|
config: {
|
|
679
|
-
policies: []
|
|
680
|
-
auth: false
|
|
1298
|
+
policies: []
|
|
681
1299
|
}
|
|
682
1300
|
},
|
|
683
1301
|
{
|
|
684
1302
|
method: "GET",
|
|
685
|
-
path: "/
|
|
686
|
-
handler: "
|
|
1303
|
+
path: "/payments",
|
|
1304
|
+
handler: "stripe.listPayments",
|
|
687
1305
|
config: {
|
|
688
|
-
policies: []
|
|
689
|
-
auth: false
|
|
1306
|
+
policies: []
|
|
690
1307
|
}
|
|
691
1308
|
},
|
|
1309
|
+
// Customer Operations
|
|
692
1310
|
{
|
|
693
1311
|
method: "POST",
|
|
694
|
-
path: "/
|
|
695
|
-
handler: "
|
|
1312
|
+
path: "/customers",
|
|
1313
|
+
handler: "stripe.createCustomer",
|
|
696
1314
|
config: {
|
|
697
|
-
policies: []
|
|
698
|
-
auth: false
|
|
1315
|
+
policies: []
|
|
699
1316
|
}
|
|
700
1317
|
},
|
|
701
1318
|
{
|
|
702
1319
|
method: "GET",
|
|
703
|
-
path: "/
|
|
704
|
-
handler: "
|
|
1320
|
+
path: "/customers/:id",
|
|
1321
|
+
handler: "stripe.retrieveCustomer",
|
|
705
1322
|
config: {
|
|
706
|
-
policies: []
|
|
707
|
-
auth: false
|
|
1323
|
+
policies: []
|
|
708
1324
|
}
|
|
709
1325
|
},
|
|
710
1326
|
{
|
|
711
|
-
method: "
|
|
712
|
-
path: "/
|
|
713
|
-
handler: "
|
|
1327
|
+
method: "GET",
|
|
1328
|
+
path: "/customers",
|
|
1329
|
+
handler: "stripe.listCustomers",
|
|
714
1330
|
config: {
|
|
715
|
-
policies: []
|
|
716
|
-
auth: false
|
|
1331
|
+
policies: []
|
|
717
1332
|
}
|
|
718
1333
|
},
|
|
1334
|
+
// Order Operations
|
|
719
1335
|
{
|
|
720
1336
|
method: "POST",
|
|
721
|
-
path: "/
|
|
722
|
-
handler: "
|
|
1337
|
+
path: "/orders",
|
|
1338
|
+
handler: "stripe.createOrder",
|
|
723
1339
|
config: {
|
|
724
|
-
policies: []
|
|
725
|
-
auth: false
|
|
1340
|
+
policies: []
|
|
726
1341
|
}
|
|
727
1342
|
},
|
|
728
1343
|
{
|
|
729
|
-
method: "
|
|
730
|
-
path: "/
|
|
731
|
-
handler: "
|
|
1344
|
+
method: "GET",
|
|
1345
|
+
path: "/orders/:id",
|
|
1346
|
+
handler: "stripe.getOrderDetails",
|
|
732
1347
|
config: {
|
|
733
|
-
policies: []
|
|
734
|
-
auth: false
|
|
1348
|
+
policies: []
|
|
735
1349
|
}
|
|
736
1350
|
},
|
|
737
1351
|
{
|
|
738
|
-
method: "
|
|
739
|
-
path: "/
|
|
740
|
-
handler: "
|
|
1352
|
+
method: "GET",
|
|
1353
|
+
path: "/orders",
|
|
1354
|
+
handler: "stripe.listOrders",
|
|
741
1355
|
config: {
|
|
742
|
-
policies: []
|
|
743
|
-
auth: false
|
|
1356
|
+
policies: []
|
|
744
1357
|
}
|
|
745
1358
|
},
|
|
1359
|
+
// Refund Operations
|
|
746
1360
|
{
|
|
747
1361
|
method: "POST",
|
|
748
|
-
path: "/
|
|
749
|
-
handler: "
|
|
1362
|
+
path: "/payments/:id/refund",
|
|
1363
|
+
handler: "stripe.createRefund",
|
|
1364
|
+
config: {
|
|
1365
|
+
policies: []
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
]
|
|
1369
|
+
};
|
|
1370
|
+
const routes$1 = {
|
|
1371
|
+
type: "admin",
|
|
1372
|
+
routes: [
|
|
1373
|
+
// Admin Dashboard and Analytics
|
|
1374
|
+
{
|
|
1375
|
+
method: "GET",
|
|
1376
|
+
path: "/admin/dashboard",
|
|
1377
|
+
handler: "stripe.getDashboardData",
|
|
750
1378
|
config: {
|
|
751
|
-
policies: [],
|
|
752
|
-
|
|
1379
|
+
policies: ["admin::isAuthenticatedAdmin"],
|
|
1380
|
+
middlewares: []
|
|
753
1381
|
}
|
|
754
1382
|
},
|
|
1383
|
+
// Payment Reports
|
|
755
1384
|
{
|
|
756
|
-
method: "
|
|
757
|
-
path: "/
|
|
758
|
-
handler: "
|
|
1385
|
+
method: "GET",
|
|
1386
|
+
path: "/admin/reports",
|
|
1387
|
+
handler: "stripe.getPaymentReports",
|
|
759
1388
|
config: {
|
|
760
|
-
policies: [],
|
|
761
|
-
|
|
1389
|
+
policies: ["admin::isAuthenticatedAdmin"],
|
|
1390
|
+
middlewares: []
|
|
762
1391
|
}
|
|
763
1392
|
},
|
|
1393
|
+
// Configuration Management
|
|
764
1394
|
{
|
|
765
1395
|
method: "GET",
|
|
766
|
-
path: "/
|
|
767
|
-
handler: "
|
|
1396
|
+
path: "/admin/config",
|
|
1397
|
+
handler: "stripe.getPluginConfig",
|
|
768
1398
|
config: {
|
|
769
|
-
policies: [],
|
|
770
|
-
|
|
1399
|
+
policies: ["admin::isAuthenticatedAdmin"],
|
|
1400
|
+
middlewares: []
|
|
771
1401
|
}
|
|
772
1402
|
},
|
|
773
1403
|
{
|
|
774
|
-
method: "
|
|
775
|
-
path: "/
|
|
776
|
-
handler: "
|
|
1404
|
+
method: "PUT",
|
|
1405
|
+
path: "/admin/config",
|
|
1406
|
+
handler: "stripe.updatePluginConfig",
|
|
1407
|
+
config: {
|
|
1408
|
+
policies: ["admin::isAuthenticatedAdmin"],
|
|
1409
|
+
middlewares: []
|
|
1410
|
+
}
|
|
1411
|
+
},
|
|
1412
|
+
// Admin Analytics
|
|
1413
|
+
{
|
|
1414
|
+
method: "GET",
|
|
1415
|
+
path: "/admin/analytics",
|
|
1416
|
+
handler: "stripe.getAnalytics",
|
|
777
1417
|
config: {
|
|
778
|
-
policies: [],
|
|
779
|
-
|
|
1418
|
+
policies: ["admin::isAuthenticatedAdmin"],
|
|
1419
|
+
middlewares: []
|
|
780
1420
|
}
|
|
781
1421
|
},
|
|
1422
|
+
// Admin Payment Management
|
|
782
1423
|
{
|
|
783
1424
|
method: "POST",
|
|
784
|
-
path: "/
|
|
785
|
-
handler: "
|
|
1425
|
+
path: "/admin/payments/:id/refund",
|
|
1426
|
+
handler: "stripe.adminCreateRefund",
|
|
786
1427
|
config: {
|
|
787
|
-
policies: [],
|
|
788
|
-
|
|
1428
|
+
policies: ["admin::isAuthenticatedAdmin"],
|
|
1429
|
+
middlewares: []
|
|
789
1430
|
}
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
1431
|
+
},
|
|
1432
|
+
{
|
|
1433
|
+
method: "GET",
|
|
1434
|
+
path: "/admin/payments",
|
|
1435
|
+
handler: "stripe.adminListPayments",
|
|
1436
|
+
config: {
|
|
1437
|
+
policies: ["admin::isAuthenticatedAdmin"],
|
|
1438
|
+
middlewares: []
|
|
1439
|
+
}
|
|
1440
|
+
},
|
|
1441
|
+
// Admin Customer Management
|
|
1442
|
+
{
|
|
1443
|
+
method: "GET",
|
|
1444
|
+
path: "/admin/customers",
|
|
1445
|
+
handler: "stripe.adminListCustomers",
|
|
1446
|
+
config: {
|
|
1447
|
+
policies: ["admin::isAuthenticatedAdmin"],
|
|
1448
|
+
middlewares: []
|
|
1449
|
+
}
|
|
1450
|
+
},
|
|
1451
|
+
// Admin Order Management
|
|
1452
|
+
{
|
|
1453
|
+
method: "GET",
|
|
1454
|
+
path: "/admin/orders",
|
|
1455
|
+
handler: "stripe.adminListOrders",
|
|
1456
|
+
config: {
|
|
1457
|
+
policies: ["admin::isAuthenticatedAdmin"],
|
|
1458
|
+
middlewares: []
|
|
1459
|
+
}
|
|
1460
|
+
},
|
|
1461
|
+
// Test Webhook
|
|
796
1462
|
{
|
|
797
1463
|
method: "POST",
|
|
798
|
-
path: "/
|
|
799
|
-
handler: "
|
|
1464
|
+
path: "/admin/test-webhook",
|
|
1465
|
+
handler: "stripe.testWebhook",
|
|
800
1466
|
config: {
|
|
801
|
-
policies: [],
|
|
802
|
-
|
|
1467
|
+
policies: ["admin::isAuthenticatedAdmin"],
|
|
1468
|
+
middlewares: []
|
|
803
1469
|
}
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
};
|
|
807
|
-
const contentApi = {
|
|
808
|
-
type: "content-api",
|
|
809
|
-
routes: [
|
|
1470
|
+
},
|
|
1471
|
+
// Functional Flow Initializer (Admin/Manual)
|
|
810
1472
|
{
|
|
811
1473
|
method: "POST",
|
|
812
|
-
path: "/
|
|
813
|
-
handler: "
|
|
1474
|
+
path: "/admin/initialize-payment",
|
|
1475
|
+
handler: "stripe.initializePayment",
|
|
814
1476
|
config: {
|
|
815
|
-
policies: []
|
|
1477
|
+
policies: ["admin::isAuthenticatedAdmin"],
|
|
1478
|
+
middlewares: []
|
|
816
1479
|
}
|
|
817
1480
|
},
|
|
1481
|
+
// Sample Payment (for Dashboard)
|
|
818
1482
|
{
|
|
819
1483
|
method: "POST",
|
|
820
|
-
path: "/
|
|
821
|
-
handler: "
|
|
1484
|
+
path: "/admin/create-sample-payment",
|
|
1485
|
+
handler: "stripe.createSamplePayment",
|
|
822
1486
|
config: {
|
|
823
|
-
policies: []
|
|
1487
|
+
policies: ["admin::isAuthenticatedAdmin"],
|
|
1488
|
+
middlewares: []
|
|
824
1489
|
}
|
|
825
1490
|
}
|
|
826
1491
|
]
|
|
827
1492
|
};
|
|
828
|
-
const productRoutes = factories.createCoreRouter("plugin::strapi-payment-plugin.product", {
|
|
829
|
-
config: {}
|
|
830
|
-
});
|
|
831
|
-
const subscriptionRoutes = factories.createCoreRouter("plugin::strapi-payment-plugin.subscription");
|
|
832
1493
|
const routes = {
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
contentApi,
|
|
836
|
-
productRoutes,
|
|
837
|
-
subscriptionRoutes
|
|
1494
|
+
"content-api": routes$2,
|
|
1495
|
+
admin: routes$1
|
|
838
1496
|
};
|
|
839
|
-
const
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
return {
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
}
|
|
847
|
-
async
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
return {
|
|
906
|
-
status: "disabled",
|
|
907
|
-
message: "PayPal functionality is currently disabled"
|
|
908
|
-
};
|
|
909
|
-
},
|
|
910
|
-
async getTransactions(limit, page) {
|
|
911
|
-
console.warn("PayPal is disabled");
|
|
912
|
-
return {
|
|
913
|
-
status: "disabled",
|
|
914
|
-
message: "PayPal functionality is currently disabled"
|
|
915
|
-
};
|
|
916
|
-
}
|
|
917
|
-
};
|
|
918
|
-
const drivers = {
|
|
919
|
-
stripe: stripeDriver,
|
|
920
|
-
paypal: paypalDriver
|
|
921
|
-
};
|
|
922
|
-
const service = ({ strapi }) => ({
|
|
923
|
-
async initializePayment(gateway, amount, currency, orderId, options) {
|
|
924
|
-
const driver = drivers[gateway];
|
|
925
|
-
if (!driver) throw new Error("Invalid payment gateway");
|
|
926
|
-
return driver.initializePayment(amount, currency, orderId, options);
|
|
927
|
-
},
|
|
928
|
-
async processWebhook(gateway, payload, signature) {
|
|
929
|
-
const driver = drivers[gateway];
|
|
930
|
-
if (!driver) throw new Error("Invalid payment gateway");
|
|
931
|
-
return driver.processWebhook(payload, signature);
|
|
932
|
-
},
|
|
933
|
-
async refundPayment(gateway, transactionId, amount, options) {
|
|
934
|
-
const driver = drivers[gateway];
|
|
935
|
-
if (!driver) throw new Error("Invalid payment gateway");
|
|
936
|
-
return driver.refundPayment(transactionId, amount, options);
|
|
937
|
-
},
|
|
938
|
-
async getPaymentDetails(gateway, transactionId) {
|
|
939
|
-
const driver = drivers[gateway];
|
|
940
|
-
if (!driver) throw new Error("Invalid payment gateway");
|
|
941
|
-
return driver.getPaymentDetails(transactionId);
|
|
942
|
-
},
|
|
943
|
-
async getTransactions(gateway, status, limit = 10, offset = 0) {
|
|
944
|
-
if (gateway == "all") {
|
|
945
|
-
const allTransactions = [];
|
|
946
|
-
for (const key of Object.keys(drivers)) {
|
|
947
|
-
const transactions = await this.getTransactions(key, status, limit, offset);
|
|
948
|
-
allTransactions.push(...transactions.data);
|
|
949
|
-
}
|
|
950
|
-
return {
|
|
951
|
-
data: allTransactions,
|
|
952
|
-
meta: {
|
|
953
|
-
total: allTransactions.length,
|
|
954
|
-
limit,
|
|
955
|
-
offset
|
|
956
|
-
}
|
|
1497
|
+
const stripeService = ({ strapi: strapi2 }) => {
|
|
1498
|
+
let stripe = null;
|
|
1499
|
+
const getConfig = () => {
|
|
1500
|
+
return strapi2.config.get("plugin::payment-plugin") || {};
|
|
1501
|
+
};
|
|
1502
|
+
const getLogger = () => {
|
|
1503
|
+
return strapi2.log;
|
|
1504
|
+
};
|
|
1505
|
+
const getCombinedConfig = async () => {
|
|
1506
|
+
const staticConfig = strapi2.config.get("plugin::payment-plugin") || {};
|
|
1507
|
+
try {
|
|
1508
|
+
const store = strapi2.store({ type: "plugin", name: "payment-plugin" });
|
|
1509
|
+
const dbConfig = await store.get({ key: "settings" });
|
|
1510
|
+
const safeDbConfig = dbConfig && typeof dbConfig === "object" ? dbConfig : {};
|
|
1511
|
+
return { ...staticConfig, ...safeDbConfig };
|
|
1512
|
+
} catch (error) {
|
|
1513
|
+
return staticConfig;
|
|
1514
|
+
}
|
|
1515
|
+
};
|
|
1516
|
+
const initializeStripe = async () => {
|
|
1517
|
+
if (stripe) {
|
|
1518
|
+
return stripe;
|
|
1519
|
+
}
|
|
1520
|
+
const config2 = await getCombinedConfig();
|
|
1521
|
+
const stripeConfig = config2.stripe || {};
|
|
1522
|
+
const secretKey = stripeConfig.secretKey || process.env.STRIPE_SECRET_KEY || (process.env.STRIPE_WEBHOOK_SECRET?.startsWith("sk_") ? process.env.STRIPE_WEBHOOK_SECRET : null) || (process.env.STRIPE_API_KEY?.startsWith("sk_") ? process.env.STRIPE_API_KEY : null);
|
|
1523
|
+
if (!secretKey) {
|
|
1524
|
+
throw new Error("Stripe Secret Key not found. Please configure it in the plugin settings or set the STRIPE_SECRET_KEY environment variable.");
|
|
1525
|
+
}
|
|
1526
|
+
const apiVersion = stripeConfig.apiVersion || "2024-06-20";
|
|
1527
|
+
stripe = new Stripe(secretKey, {
|
|
1528
|
+
apiVersion
|
|
1529
|
+
});
|
|
1530
|
+
getLogger().info("Stripe service initialized successfully");
|
|
1531
|
+
return stripe;
|
|
1532
|
+
};
|
|
1533
|
+
const getStripe = () => {
|
|
1534
|
+
return stripe;
|
|
1535
|
+
};
|
|
1536
|
+
const mapStripeStatusToStrapi = (stripeStatus) => {
|
|
1537
|
+
switch (stripeStatus) {
|
|
1538
|
+
case "succeeded":
|
|
1539
|
+
return "succeeded";
|
|
1540
|
+
case "canceled":
|
|
1541
|
+
return "canceled";
|
|
1542
|
+
case "processing":
|
|
1543
|
+
case "requires_action":
|
|
1544
|
+
case "requires_capture":
|
|
1545
|
+
case "requires_confirmation":
|
|
1546
|
+
case "requires_payment_method":
|
|
1547
|
+
return "pending";
|
|
1548
|
+
default:
|
|
1549
|
+
return "pending";
|
|
1550
|
+
}
|
|
1551
|
+
};
|
|
1552
|
+
const createPaymentIntent = async (params) => {
|
|
1553
|
+
try {
|
|
1554
|
+
const stripe2 = await initializeStripe();
|
|
1555
|
+
const config2 = await getCombinedConfig();
|
|
1556
|
+
const logger = getLogger();
|
|
1557
|
+
const { amount, currency, metadata = {}, customer: customer2, orderId, customerId, payment_method, confirm } = params;
|
|
1558
|
+
const enhancedMetadata = {
|
|
1559
|
+
...metadata,
|
|
1560
|
+
...orderId && { strapi_order_id: orderId.toString() },
|
|
1561
|
+
...customerId && { strapi_customer_id: customerId.toString() },
|
|
1562
|
+
source: "strapi_payment_plugin"
|
|
957
1563
|
};
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1564
|
+
const paymentIntentData = {
|
|
1565
|
+
amount,
|
|
1566
|
+
currency,
|
|
1567
|
+
metadata: enhancedMetadata,
|
|
1568
|
+
automatic_payment_methods: {
|
|
1569
|
+
enabled: true,
|
|
1570
|
+
...confirm && { allow_redirects: "never" }
|
|
965
1571
|
}
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1572
|
+
};
|
|
1573
|
+
if (customer2) {
|
|
1574
|
+
paymentIntentData.customer = customer2;
|
|
969
1575
|
}
|
|
970
|
-
if (
|
|
971
|
-
|
|
1576
|
+
if (payment_method) {
|
|
1577
|
+
paymentIntentData.payment_method = payment_method;
|
|
972
1578
|
}
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
amount = parseFloat(txn.amount_with_breakdown?.gross_amount?.value || "0");
|
|
984
|
-
currency = txn.amount_with_breakdown?.gross_amount?.currency_code || "USD";
|
|
985
|
-
customer = txn.payer_info?.email_address || "unknown";
|
|
986
|
-
paymentMethod = txn.payment_source?.paypal?.name || "paypal";
|
|
987
|
-
txnStatus = txn.status;
|
|
988
|
-
date = new Date(txn.create_time).toISOString();
|
|
989
|
-
}
|
|
990
|
-
return {
|
|
991
|
-
id: txn.id,
|
|
992
|
-
amount,
|
|
993
|
-
currency,
|
|
994
|
-
status: txnStatus,
|
|
995
|
-
date,
|
|
996
|
-
customer,
|
|
997
|
-
paymentMethod,
|
|
998
|
-
gateway
|
|
999
|
-
};
|
|
1579
|
+
if (confirm) {
|
|
1580
|
+
paymentIntentData.confirm = confirm;
|
|
1581
|
+
}
|
|
1582
|
+
const paymentIntent = await stripe2.paymentIntents.create(paymentIntentData);
|
|
1583
|
+
logger.info("Payment intent created", {
|
|
1584
|
+
paymentIntentId: paymentIntent.id,
|
|
1585
|
+
amount,
|
|
1586
|
+
currency,
|
|
1587
|
+
orderId,
|
|
1588
|
+
customerId
|
|
1000
1589
|
});
|
|
1001
|
-
return
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1590
|
+
return paymentIntent;
|
|
1591
|
+
} catch (error) {
|
|
1592
|
+
const logger = getLogger();
|
|
1593
|
+
logger.error("Failed to create payment intent", { error, params });
|
|
1594
|
+
throw error;
|
|
1595
|
+
}
|
|
1596
|
+
};
|
|
1597
|
+
const confirmPayment = async (paymentIntentId) => {
|
|
1598
|
+
try {
|
|
1599
|
+
const stripe2 = await initializeStripe();
|
|
1600
|
+
const logger = getLogger();
|
|
1601
|
+
const paymentIntent = await stripe2.paymentIntents.confirm(paymentIntentId);
|
|
1602
|
+
logger.info("Payment confirmed", { paymentIntentId, status: paymentIntent.status });
|
|
1603
|
+
return paymentIntent;
|
|
1604
|
+
} catch (error) {
|
|
1605
|
+
const logger = getLogger();
|
|
1606
|
+
logger.error("Failed to confirm payment", { error, paymentIntentId });
|
|
1607
|
+
throw error;
|
|
1608
|
+
}
|
|
1609
|
+
};
|
|
1610
|
+
const createCustomer = async (params) => {
|
|
1611
|
+
try {
|
|
1612
|
+
const stripe2 = await initializeStripe();
|
|
1613
|
+
const logger = getLogger();
|
|
1614
|
+
const { email, firstName, lastName, phone, address, metadata = {}, strapiCustomerId } = params;
|
|
1615
|
+
const customerData = {
|
|
1616
|
+
email,
|
|
1617
|
+
name: `${firstName} ${lastName}`,
|
|
1618
|
+
phone,
|
|
1619
|
+
address,
|
|
1620
|
+
metadata: {
|
|
1621
|
+
...metadata,
|
|
1622
|
+
...strapiCustomerId && { strapi_customer_id: strapiCustomerId.toString() },
|
|
1623
|
+
first_name: firstName,
|
|
1624
|
+
last_name: lastName,
|
|
1625
|
+
source: "strapi_payment_plugin"
|
|
1007
1626
|
}
|
|
1008
1627
|
};
|
|
1009
|
-
|
|
1010
|
-
|
|
1628
|
+
const customer2 = await stripe2.customers.create(customerData);
|
|
1629
|
+
logger.info("Customer created in Stripe", {
|
|
1630
|
+
customerId: customer2.id,
|
|
1631
|
+
email,
|
|
1632
|
+
strapiCustomerId
|
|
1633
|
+
});
|
|
1634
|
+
return customer2;
|
|
1635
|
+
} catch (error) {
|
|
1636
|
+
const logger = getLogger();
|
|
1637
|
+
logger.error("Failed to create customer", { error, params });
|
|
1638
|
+
throw error;
|
|
1011
1639
|
}
|
|
1012
|
-
}
|
|
1013
|
-
async
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
stripe: stripeConfigured,
|
|
1025
|
-
paypal: paypalConfigured
|
|
1026
|
-
};
|
|
1027
|
-
}
|
|
1028
|
-
});
|
|
1029
|
-
const product = factories.createCoreService("plugin::strapi-payment-plugin.product", ({ strapi }) => ({
|
|
1030
|
-
async createProduct(productData) {
|
|
1031
|
-
const strapiProduct = await strapi.db.query("plugin::strapi-payment-plugin.product").create({
|
|
1032
|
-
data: productData
|
|
1033
|
-
});
|
|
1034
|
-
if (config.stripe.secretKey) {
|
|
1035
|
-
try {
|
|
1036
|
-
const stripeProduct = await stripeDriver.createProduct(productData);
|
|
1037
|
-
await strapi.db.query("plugin::strapi-payment-plugin.product").update({
|
|
1038
|
-
where: { id: strapiProduct.id },
|
|
1039
|
-
data: {
|
|
1040
|
-
stripeProductId: stripeProduct.id
|
|
1041
|
-
}
|
|
1042
|
-
});
|
|
1043
|
-
} catch (error) {
|
|
1044
|
-
console.error("Error creating product in Stripe:", error);
|
|
1045
|
-
}
|
|
1640
|
+
};
|
|
1641
|
+
const retrieveCustomer = async (customerId) => {
|
|
1642
|
+
try {
|
|
1643
|
+
const stripe2 = await initializeStripe();
|
|
1644
|
+
const logger = getLogger();
|
|
1645
|
+
const customer2 = await stripe2.customers.retrieve(customerId);
|
|
1646
|
+
logger.info("Customer retrieved from Stripe", { customerId });
|
|
1647
|
+
return customer2;
|
|
1648
|
+
} catch (error) {
|
|
1649
|
+
const logger = getLogger();
|
|
1650
|
+
logger.error("Failed to retrieve customer", { error, customerId });
|
|
1651
|
+
throw error;
|
|
1046
1652
|
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1653
|
+
};
|
|
1654
|
+
const createRefund = async (params) => {
|
|
1655
|
+
try {
|
|
1656
|
+
const stripe2 = await initializeStripe();
|
|
1657
|
+
const logger = getLogger();
|
|
1658
|
+
const { paymentIntentId, amount, reason, metadata = {} } = params;
|
|
1659
|
+
const refundData = {
|
|
1660
|
+
payment_intent: paymentIntentId,
|
|
1661
|
+
metadata: {
|
|
1662
|
+
...metadata,
|
|
1663
|
+
source: "strapi_payment_plugin",
|
|
1664
|
+
refunded_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1665
|
+
}
|
|
1666
|
+
};
|
|
1667
|
+
if (amount) {
|
|
1668
|
+
refundData.amount = amount;
|
|
1052
1669
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
where: { id: strapiProduct.id }
|
|
1056
|
-
});
|
|
1057
|
-
},
|
|
1058
|
-
async updateProduct(id, productData) {
|
|
1059
|
-
const updatedProduct = await strapi.db.query("plugin::strapi-payment-plugin.product").update({
|
|
1060
|
-
where: { id },
|
|
1061
|
-
data: productData
|
|
1062
|
-
});
|
|
1063
|
-
if (updatedProduct.stripeProductId && config.stripe.secretKey) {
|
|
1064
|
-
try {
|
|
1065
|
-
await stripeDriver.updateProduct(updatedProduct.stripeProductId, productData);
|
|
1066
|
-
} catch (error) {
|
|
1067
|
-
console.error("Error updating product in Stripe:", error);
|
|
1670
|
+
if (reason) {
|
|
1671
|
+
refundData.reason = reason;
|
|
1068
1672
|
}
|
|
1673
|
+
const refund = await stripe2.refunds.create(refundData);
|
|
1674
|
+
logger.info("Refund processed", {
|
|
1675
|
+
refundId: refund.id,
|
|
1676
|
+
paymentIntentId,
|
|
1677
|
+
amount: refund.amount
|
|
1678
|
+
});
|
|
1679
|
+
return refund;
|
|
1680
|
+
} catch (error) {
|
|
1681
|
+
const logger = getLogger();
|
|
1682
|
+
logger.error("Failed to create refund", { error, params });
|
|
1683
|
+
throw error;
|
|
1069
1684
|
}
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1685
|
+
};
|
|
1686
|
+
const constructEvent = async (payload, signature) => {
|
|
1687
|
+
const stripe2 = await initializeStripe();
|
|
1688
|
+
const config2 = await getCombinedConfig();
|
|
1689
|
+
const stripeConfig = config2.stripe || {};
|
|
1690
|
+
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET || stripeConfig.webhookSecret;
|
|
1691
|
+
if (!webhookSecret) {
|
|
1692
|
+
throw new Error("Stripe webhook secret not configured. Please set STRIPE_WEBHOOK_SECRET environment variable.");
|
|
1076
1693
|
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
})
|
|
1080
|
-
const
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
let customerId = subscriptionData.stripeCustomerId;
|
|
1091
|
-
if (!customerId) {
|
|
1092
|
-
const customer = await stripeDriver.customers.create({
|
|
1093
|
-
email: subscriptionData.email || "customer@example.com",
|
|
1094
|
-
name: subscriptionData.customerName || "Customer"
|
|
1095
|
-
});
|
|
1096
|
-
customerId = customer.id;
|
|
1097
|
-
await strapi.db.query("plugin::strapi-payment-plugin.subscription").update({
|
|
1098
|
-
where: { id: strapiSubscription.id },
|
|
1099
|
-
data: {
|
|
1100
|
-
stripeCustomerId: customerId
|
|
1101
|
-
}
|
|
1102
|
-
});
|
|
1694
|
+
try {
|
|
1695
|
+
return stripe2.webhooks.constructEvent(payload, signature, webhookSecret);
|
|
1696
|
+
} catch (error) {
|
|
1697
|
+
const logger = getLogger();
|
|
1698
|
+
logger.error("Failed to construct webhook event", { error });
|
|
1699
|
+
throw error;
|
|
1700
|
+
}
|
|
1701
|
+
};
|
|
1702
|
+
const updateStrapiPayment = async (stripePaymentIntentId, data) => {
|
|
1703
|
+
try {
|
|
1704
|
+
const existingPayments = await strapi2.documents("plugin::payment-plugin.payment").findMany({
|
|
1705
|
+
filters: {
|
|
1706
|
+
stripe_payment_intent_id: stripePaymentIntentId
|
|
1103
1707
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
}
|
|
1110
|
-
);
|
|
1111
|
-
await strapi.db.query("plugin::strapi-payment-plugin.subscription").update({
|
|
1112
|
-
where: { id: strapiSubscription.id },
|
|
1113
|
-
data: {
|
|
1114
|
-
stripeSubscriptionId: stripeSubscription.id
|
|
1115
|
-
}
|
|
1708
|
+
});
|
|
1709
|
+
if (existingPayments.length > 0) {
|
|
1710
|
+
await strapi2.documents("plugin::payment-plugin.payment").update({
|
|
1711
|
+
documentId: existingPayments[0].documentId,
|
|
1712
|
+
data
|
|
1116
1713
|
});
|
|
1117
|
-
} catch (error) {
|
|
1118
|
-
console.error("Error creating subscription in Stripe:", error);
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
if (product2.paypalProductId && config.paypal.clientId && config.paypal.clientSecret) {
|
|
1122
|
-
try {
|
|
1123
|
-
console.warn("PayPal is disabled");
|
|
1124
|
-
} catch (error) {
|
|
1125
|
-
console.error("Error creating subscription in PayPal:", error);
|
|
1126
1714
|
}
|
|
1715
|
+
} catch (error) {
|
|
1716
|
+
const logger = getLogger();
|
|
1717
|
+
logger.error("Failed to update Strapi payment record", {
|
|
1718
|
+
error,
|
|
1719
|
+
stripePaymentIntentId,
|
|
1720
|
+
data
|
|
1721
|
+
});
|
|
1127
1722
|
}
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
} catch (error) {
|
|
1143
|
-
console.error("Error updating subscription in Stripe:", error);
|
|
1144
|
-
}
|
|
1723
|
+
};
|
|
1724
|
+
const updateStrapiOrder = async (orderId, data) => {
|
|
1725
|
+
try {
|
|
1726
|
+
await strapi2.documents("plugin::payment-plugin.order").update({
|
|
1727
|
+
documentId: orderId,
|
|
1728
|
+
data
|
|
1729
|
+
});
|
|
1730
|
+
} catch (error) {
|
|
1731
|
+
const logger = getLogger();
|
|
1732
|
+
logger.error("Failed to update Strapi order record", {
|
|
1733
|
+
error,
|
|
1734
|
+
orderId,
|
|
1735
|
+
data
|
|
1736
|
+
});
|
|
1145
1737
|
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1738
|
+
};
|
|
1739
|
+
const updateStrapiCustomer = async (customerId, data) => {
|
|
1740
|
+
try {
|
|
1741
|
+
await strapi2.documents("plugin::payment-plugin.customer").update({
|
|
1742
|
+
documentId: customerId,
|
|
1743
|
+
data
|
|
1744
|
+
});
|
|
1745
|
+
} catch (error) {
|
|
1746
|
+
const logger = getLogger();
|
|
1747
|
+
logger.error("Failed to update Strapi customer record", {
|
|
1748
|
+
error,
|
|
1749
|
+
customerId,
|
|
1750
|
+
data
|
|
1751
|
+
});
|
|
1157
1752
|
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
where: { id }
|
|
1753
|
+
};
|
|
1754
|
+
const createCustomerRecord = async (data) => {
|
|
1755
|
+
return strapi2.documents("plugin::payment-plugin.customer").create({
|
|
1756
|
+
data
|
|
1163
1757
|
});
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
if (subscription2.stripeSubscriptionId && config.stripe.secretKey) {
|
|
1168
|
-
try {
|
|
1169
|
-
await stripeDriver.cancelSubscription(subscription2.stripeSubscriptionId);
|
|
1170
|
-
} catch (error) {
|
|
1171
|
-
console.error("Error canceling subscription in Stripe:", error);
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
if (subscription2.paypalSubscriptionId && config.paypal.clientId && config.paypal.clientSecret) {
|
|
1175
|
-
try {
|
|
1176
|
-
console.warn("PayPal is disabled");
|
|
1177
|
-
} catch (error) {
|
|
1178
|
-
console.error("Error canceling subscription in PayPal:", error);
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
return strapi.db.query("plugin::strapi-payment-plugin.subscription").update({
|
|
1182
|
-
where: { id },
|
|
1758
|
+
};
|
|
1759
|
+
const createOrderRecord = async (data) => {
|
|
1760
|
+
return strapi2.documents("plugin::payment-plugin.order").create({
|
|
1183
1761
|
data: {
|
|
1184
|
-
|
|
1185
|
-
|
|
1762
|
+
...data,
|
|
1763
|
+
status: data.status || "pending"
|
|
1186
1764
|
}
|
|
1187
1765
|
});
|
|
1188
|
-
}
|
|
1189
|
-
async
|
|
1190
|
-
|
|
1191
|
-
|
|
1766
|
+
};
|
|
1767
|
+
const createStrapiPaymentRecord = async (paymentIntent) => {
|
|
1768
|
+
try {
|
|
1769
|
+
const metadata = paymentIntent.metadata || {};
|
|
1770
|
+
const customerId = metadata.strapi_customer_id;
|
|
1771
|
+
const orderId = metadata.strapi_order_id;
|
|
1772
|
+
const paymentData = {
|
|
1773
|
+
stripe_payment_intent_id: paymentIntent.id,
|
|
1774
|
+
amount: paymentIntent.amount / 100,
|
|
1775
|
+
currency: paymentIntent.currency,
|
|
1776
|
+
status: mapStripeStatusToStrapi(paymentIntent.status),
|
|
1777
|
+
payment_method: paymentIntent.payment_method || "card",
|
|
1778
|
+
metadata: paymentIntent.metadata,
|
|
1779
|
+
customer: customerId,
|
|
1780
|
+
order: orderId
|
|
1781
|
+
};
|
|
1782
|
+
const record = await strapi2.documents("plugin::payment-plugin.payment").create({
|
|
1783
|
+
data: paymentData
|
|
1784
|
+
});
|
|
1785
|
+
getLogger().info("Created Strapi payment record", {
|
|
1786
|
+
paymentIntentId: paymentIntent.id,
|
|
1787
|
+
strapiOrderId: orderId,
|
|
1788
|
+
strapiCustomerId: customerId
|
|
1789
|
+
});
|
|
1790
|
+
return record;
|
|
1791
|
+
} catch (error) {
|
|
1792
|
+
const logger = getLogger();
|
|
1793
|
+
logger.error("Failed to create Strapi payment record", {
|
|
1794
|
+
error,
|
|
1795
|
+
paymentIntentId: paymentIntent.id
|
|
1796
|
+
});
|
|
1797
|
+
throw error;
|
|
1798
|
+
}
|
|
1799
|
+
};
|
|
1800
|
+
const initializePaymentFlow = async (params) => {
|
|
1801
|
+
const {
|
|
1802
|
+
amount,
|
|
1803
|
+
currency,
|
|
1804
|
+
email,
|
|
1805
|
+
firstName,
|
|
1806
|
+
lastName,
|
|
1807
|
+
items = [{ name: "Default Item", amount, quantity: 1 }],
|
|
1808
|
+
metadata = {},
|
|
1809
|
+
confirm = false,
|
|
1810
|
+
payment_method,
|
|
1811
|
+
orderNumber = `ORD-${Date.now()}`
|
|
1812
|
+
} = params;
|
|
1813
|
+
const stripeCustomer = await createCustomer({
|
|
1814
|
+
email,
|
|
1815
|
+
firstName,
|
|
1816
|
+
lastName
|
|
1817
|
+
});
|
|
1818
|
+
const customers = await strapi2.documents("plugin::payment-plugin.customer").findMany({
|
|
1819
|
+
filters: { email: { $eq: email } }
|
|
1192
1820
|
});
|
|
1193
|
-
|
|
1194
|
-
|
|
1821
|
+
let strapiCustomer;
|
|
1822
|
+
if (customers.length > 0) {
|
|
1823
|
+
strapiCustomer = customers[0];
|
|
1824
|
+
await updateStrapiCustomer(strapiCustomer.documentId, {
|
|
1825
|
+
stripe_customer_id: stripeCustomer.id
|
|
1826
|
+
});
|
|
1827
|
+
} else {
|
|
1828
|
+
strapiCustomer = await createCustomerRecord({
|
|
1829
|
+
email,
|
|
1830
|
+
first_name: firstName,
|
|
1831
|
+
last_name: lastName,
|
|
1832
|
+
stripe_customer_id: stripeCustomer.id
|
|
1833
|
+
});
|
|
1195
1834
|
}
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1835
|
+
const strapiOrder = await createOrderRecord({
|
|
1836
|
+
order_number: orderNumber,
|
|
1837
|
+
total_amount: amount / 100,
|
|
1838
|
+
currency,
|
|
1839
|
+
customer: strapiCustomer.documentId,
|
|
1840
|
+
items,
|
|
1841
|
+
metadata
|
|
1842
|
+
});
|
|
1843
|
+
const paymentIntent = await createPaymentIntent({
|
|
1844
|
+
amount,
|
|
1845
|
+
currency,
|
|
1846
|
+
customerId: strapiCustomer.documentId,
|
|
1847
|
+
orderId: strapiOrder.documentId,
|
|
1848
|
+
customer: stripeCustomer.id,
|
|
1849
|
+
confirm,
|
|
1850
|
+
payment_method,
|
|
1851
|
+
metadata: { ...metadata, flow: "functional_api" }
|
|
1852
|
+
});
|
|
1853
|
+
const strapiPayment = await createStrapiPaymentRecord(paymentIntent);
|
|
1854
|
+
return {
|
|
1855
|
+
payment: strapiPayment,
|
|
1856
|
+
order: strapiOrder,
|
|
1857
|
+
customer: strapiCustomer,
|
|
1858
|
+
stripePaymentIntent: paymentIntent
|
|
1859
|
+
};
|
|
1860
|
+
};
|
|
1861
|
+
const handleWebhook = async (event) => {
|
|
1862
|
+
const logger = getLogger();
|
|
1863
|
+
const config2 = getConfig();
|
|
1864
|
+
const webhookConfig = config2.logging || {};
|
|
1865
|
+
try {
|
|
1866
|
+
if (webhookConfig.enableWebhookLogging) {
|
|
1867
|
+
logger.info("Processing Stripe webhook", {
|
|
1868
|
+
eventType: event.type,
|
|
1869
|
+
eventId: event.id
|
|
1206
1870
|
});
|
|
1207
|
-
} catch (error) {
|
|
1208
|
-
console.error("Error syncing subscription status from Stripe:", error);
|
|
1209
1871
|
}
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1872
|
+
switch (event.type) {
|
|
1873
|
+
case "payment_intent.succeeded": {
|
|
1874
|
+
const paymentIntent = event.data.object;
|
|
1875
|
+
try {
|
|
1876
|
+
const existingPayments = await strapi2.documents("plugin::payment-plugin.payment").findMany({
|
|
1877
|
+
filters: {
|
|
1878
|
+
stripe_payment_intent_id: paymentIntent.id
|
|
1879
|
+
}
|
|
1880
|
+
});
|
|
1881
|
+
if (existingPayments.length > 0) {
|
|
1882
|
+
await strapi2.documents("plugin::payment-plugin.payment").update({
|
|
1883
|
+
documentId: existingPayments[0].documentId,
|
|
1884
|
+
data: {
|
|
1885
|
+
status: "succeeded",
|
|
1886
|
+
metadata: paymentIntent.metadata
|
|
1887
|
+
}
|
|
1888
|
+
});
|
|
1889
|
+
} else {
|
|
1890
|
+
await createStrapiPaymentRecord(paymentIntent);
|
|
1891
|
+
}
|
|
1892
|
+
} catch (error) {
|
|
1893
|
+
await createStrapiPaymentRecord(paymentIntent);
|
|
1220
1894
|
}
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1895
|
+
const orderId = paymentIntent.metadata?.strapi_order_id;
|
|
1896
|
+
if (orderId) {
|
|
1897
|
+
await updateStrapiOrder(orderId, {
|
|
1898
|
+
status: "completed"
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
logger.info("Payment succeeded", {
|
|
1902
|
+
paymentIntentId: paymentIntent.id,
|
|
1903
|
+
amount: paymentIntent.amount,
|
|
1904
|
+
orderId
|
|
1905
|
+
});
|
|
1906
|
+
break;
|
|
1907
|
+
}
|
|
1908
|
+
case "payment_intent.payment_failed": {
|
|
1909
|
+
const paymentIntent = event.data.object;
|
|
1910
|
+
await updateStrapiPayment(paymentIntent.id, {
|
|
1911
|
+
status: "failed",
|
|
1912
|
+
metadata: paymentIntent.metadata
|
|
1913
|
+
});
|
|
1914
|
+
const orderId = paymentIntent.metadata?.strapi_order_id;
|
|
1915
|
+
if (orderId) {
|
|
1916
|
+
await updateStrapiOrder(orderId, {
|
|
1917
|
+
status: "failed"
|
|
1918
|
+
});
|
|
1919
|
+
}
|
|
1920
|
+
logger.warn("Payment failed", {
|
|
1921
|
+
paymentIntentId: paymentIntent.id,
|
|
1922
|
+
orderId,
|
|
1923
|
+
lastError: paymentIntent.last_payment_error
|
|
1924
|
+
});
|
|
1925
|
+
break;
|
|
1926
|
+
}
|
|
1927
|
+
case "payment_intent.canceled": {
|
|
1928
|
+
const paymentIntent = event.data.object;
|
|
1929
|
+
await updateStrapiPayment(paymentIntent.id, {
|
|
1930
|
+
status: "canceled",
|
|
1931
|
+
metadata: paymentIntent.metadata
|
|
1932
|
+
});
|
|
1933
|
+
const orderId = paymentIntent.metadata?.strapi_order_id;
|
|
1934
|
+
if (orderId) {
|
|
1935
|
+
await updateStrapiOrder(orderId, {
|
|
1936
|
+
status: "failed"
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
logger.info("Payment canceled", {
|
|
1940
|
+
paymentIntentId: paymentIntent.id,
|
|
1941
|
+
orderId
|
|
1942
|
+
});
|
|
1943
|
+
break;
|
|
1944
|
+
}
|
|
1945
|
+
case "charge.refunded": {
|
|
1946
|
+
const charge = event.data.object;
|
|
1947
|
+
if (charge.payment_intent) {
|
|
1948
|
+
await updateStrapiPayment(charge.payment_intent, {
|
|
1949
|
+
status: "refunded"
|
|
1950
|
+
});
|
|
1951
|
+
const orderId = charge.metadata?.strapi_order_id;
|
|
1952
|
+
if (orderId) {
|
|
1953
|
+
await updateStrapiOrder(orderId, {
|
|
1954
|
+
status: "refunded"
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
logger.info("Charge refunded", {
|
|
1959
|
+
chargeId: charge.id,
|
|
1960
|
+
paymentIntentId: charge.payment_intent,
|
|
1961
|
+
amount: charge.amount_refunded,
|
|
1962
|
+
orderId: charge.metadata?.strapi_order_id
|
|
1963
|
+
});
|
|
1964
|
+
break;
|
|
1965
|
+
}
|
|
1966
|
+
case "customer.created": {
|
|
1967
|
+
const customer2 = event.data.object;
|
|
1968
|
+
const strapiCustomerId = customer2.metadata?.strapi_customer_id;
|
|
1969
|
+
if (strapiCustomerId) {
|
|
1970
|
+
await updateStrapiCustomer(strapiCustomerId, {
|
|
1971
|
+
stripe_customer_id: customer2.id,
|
|
1972
|
+
metadata: customer2.metadata
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
logger.info("Customer created in Stripe", {
|
|
1976
|
+
stripeCustomerId: customer2.id,
|
|
1977
|
+
strapiCustomerId
|
|
1978
|
+
});
|
|
1979
|
+
break;
|
|
1980
|
+
}
|
|
1981
|
+
default:
|
|
1982
|
+
logger.info("Unhandled webhook event type", { eventType: event.type });
|
|
1224
1983
|
}
|
|
1984
|
+
} catch (error) {
|
|
1985
|
+
logger.error("Error processing webhook", {
|
|
1986
|
+
error,
|
|
1987
|
+
eventType: event.type,
|
|
1988
|
+
eventId: event.id
|
|
1989
|
+
});
|
|
1990
|
+
throw error;
|
|
1225
1991
|
}
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1992
|
+
};
|
|
1993
|
+
return {
|
|
1994
|
+
initializeStripe,
|
|
1995
|
+
createPaymentIntent,
|
|
1996
|
+
confirmPayment,
|
|
1997
|
+
createCustomer,
|
|
1998
|
+
retrieveCustomer,
|
|
1999
|
+
createRefund,
|
|
2000
|
+
handleWebhook,
|
|
2001
|
+
constructEvent,
|
|
2002
|
+
updateStrapiOrder,
|
|
2003
|
+
updateStrapiCustomer,
|
|
2004
|
+
createStrapiPaymentRecord,
|
|
2005
|
+
createCustomerRecord,
|
|
2006
|
+
createOrderRecord,
|
|
2007
|
+
initializePaymentFlow,
|
|
2008
|
+
getStripe
|
|
2009
|
+
};
|
|
2010
|
+
};
|
|
1231
2011
|
const services = {
|
|
1232
|
-
|
|
1233
|
-
product,
|
|
1234
|
-
subscription
|
|
2012
|
+
stripe: stripeService
|
|
1235
2013
|
};
|
|
1236
2014
|
const index = {
|
|
1237
2015
|
register,
|