@reeboot/strapi-payment-plugin 0.0.9 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/_chunks/{Analytics-kChERzX4.mjs → Analytics-CPXtqS6s.mjs} +1 -1
  2. package/dist/_chunks/{Analytics-DjXqUZy1.js → Analytics-ZpHHWBQt.js} +1 -1
  3. package/dist/_chunks/{App-D5x-l0Sz.mjs → App-CZNZ1JKf.mjs} +13 -7
  4. package/dist/_chunks/{App-BpwS5-wg.js → App-CsbyykNC.js} +13 -7
  5. package/dist/_chunks/{Customers-ChrS373z.js → Customers-D4RVSZ_m.js} +1 -1
  6. package/dist/_chunks/{Customers-BYQZcZeb.mjs → Customers-DMDEBpH9.mjs} +1 -1
  7. package/dist/_chunks/{Dashboard-DUWxJQ7q.mjs → Dashboard-D-ckyWDO.mjs} +1 -1
  8. package/dist/_chunks/{Dashboard-DANhyMZK.js → Dashboard-NNqGJmYw.js} +1 -1
  9. package/dist/_chunks/{Orders-BXyc3CTW.js → Orders-BELfgToU.js} +1 -1
  10. package/dist/_chunks/{Orders-B3GZULs3.mjs → Orders-BIkRttRZ.mjs} +1 -1
  11. package/dist/_chunks/{Payments-DmAzroEv.js → Payments-CIEb4f07.js} +1 -1
  12. package/dist/_chunks/{Payments-DL6hne_y.mjs → Payments-CKlL-PCV.mjs} +1 -1
  13. package/dist/_chunks/{Settings-Ck1G0iaS.mjs → Settings-BQXkYm-c.mjs} +1 -1
  14. package/dist/_chunks/{Settings-CMEzIMqt.js → Settings-CcY98Hyw.js} +1 -1
  15. package/dist/_chunks/Subscriptions--cI3HC2x.js +178 -0
  16. package/dist/_chunks/Subscriptions-Dq8Uk7sK.mjs +178 -0
  17. package/dist/_chunks/{index-Bo7VLX9i.mjs → index-BBarHYyt.mjs} +1 -1
  18. package/dist/_chunks/{index-Cmn7Tfmf.js → index-DBZ4rcUW.js} +1 -1
  19. package/dist/admin/index.js +1 -1
  20. package/dist/admin/index.mjs +1 -1
  21. package/dist/admin/src/index.d.ts +22 -1
  22. package/dist/admin/src/pages/Subscriptions.d.ts +2 -0
  23. package/dist/admin/src/types/index.d.ts +20 -0
  24. package/dist/server/index.js +622 -46
  25. package/dist/server/index.mjs +622 -46
  26. package/dist/server/src/__tests__/setup.d.ts +316 -0
  27. package/dist/server/src/content-types/customer/index.d.ts +6 -0
  28. package/dist/server/src/content-types/index.d.ts +89 -0
  29. package/dist/server/src/content-types/subscription/index.d.ts +84 -0
  30. package/dist/server/src/controllers/index.d.ts +7 -0
  31. package/dist/server/src/controllers/stripe.d.ts +34 -2
  32. package/dist/server/src/index.d.ts +99 -6
  33. package/dist/server/src/middlewares/index.d.ts +3 -3
  34. package/dist/server/src/services/stripe.d.ts +50 -10
  35. package/dist/server/src/services/types/api.d.ts +24 -5
  36. package/dist/server/src/types/plugin-config.d.ts +360 -0
  37. package/package.json +6 -3
@@ -10,7 +10,7 @@ const config = {
10
10
  validator() {
11
11
  }
12
12
  };
13
- const schema$2 = {
13
+ const schema$3 = {
14
14
  kind: "collectionType",
15
15
  collectionName: "customers",
16
16
  info: {
@@ -73,11 +73,17 @@ const schema$2 = {
73
73
  relation: "oneToMany",
74
74
  target: "plugin::payment-plugin.payment",
75
75
  mappedBy: "customer"
76
+ },
77
+ subscriptions: {
78
+ type: "relation",
79
+ relation: "oneToMany",
80
+ target: "plugin::payment-plugin.subscription",
81
+ mappedBy: "customer"
76
82
  }
77
83
  }
78
84
  };
79
- const customer = { schema: schema$2 };
80
- const schema$1 = {
85
+ const customer = { schema: schema$3 };
86
+ const schema$2 = {
81
87
  kind: "collectionType",
82
88
  collectionName: "orders",
83
89
  info: {
@@ -162,8 +168,8 @@ const schema$1 = {
162
168
  }
163
169
  }
164
170
  };
165
- const order = { schema: schema$1 };
166
- const schema = {
171
+ const order = { schema: schema$2 };
172
+ const schema$1 = {
167
173
  kind: "collectionType",
168
174
  collectionName: "payments",
169
175
  info: {
@@ -235,17 +241,115 @@ const schema = {
235
241
  }
236
242
  }
237
243
  };
238
- const payment = { schema };
244
+ const payment = { schema: schema$1 };
245
+ const kind = "collectionType";
246
+ const collectionName = "payment_plugin_subscriptions";
247
+ const info = {
248
+ singularName: "subscription",
249
+ pluralName: "subscriptions",
250
+ displayName: "Subscription",
251
+ description: "Stripe subscription records for recurring billing"
252
+ };
253
+ const options = {
254
+ draftAndPublish: false
255
+ };
256
+ const attributes = {
257
+ stripe_subscription_id: {
258
+ type: "string",
259
+ unique: true,
260
+ required: true,
261
+ configurable: false
262
+ },
263
+ stripe_customer_id: {
264
+ type: "string",
265
+ required: true,
266
+ configurable: false
267
+ },
268
+ stripe_price_id: {
269
+ type: "string",
270
+ required: true,
271
+ configurable: false
272
+ },
273
+ stripe_product_id: {
274
+ type: "string",
275
+ configurable: false
276
+ },
277
+ status: {
278
+ type: "enumeration",
279
+ "enum": [
280
+ "active",
281
+ "canceled",
282
+ "incomplete",
283
+ "incomplete_expired",
284
+ "past_due",
285
+ "paused",
286
+ "trialing",
287
+ "unpaid"
288
+ ],
289
+ required: true,
290
+ "default": "incomplete",
291
+ configurable: false
292
+ },
293
+ current_period_start: {
294
+ type: "datetime",
295
+ configurable: false
296
+ },
297
+ current_period_end: {
298
+ type: "datetime",
299
+ configurable: false
300
+ },
301
+ cancel_at_period_end: {
302
+ type: "boolean",
303
+ "default": false,
304
+ configurable: false
305
+ },
306
+ canceled_at: {
307
+ type: "datetime",
308
+ configurable: false
309
+ },
310
+ trial_start: {
311
+ type: "datetime",
312
+ configurable: false
313
+ },
314
+ trial_end: {
315
+ type: "datetime",
316
+ configurable: false
317
+ },
318
+ latest_invoice_id: {
319
+ type: "string",
320
+ configurable: false
321
+ },
322
+ metadata: {
323
+ type: "json",
324
+ configurable: false
325
+ },
326
+ customer: {
327
+ type: "relation",
328
+ relation: "manyToOne",
329
+ target: "plugin::payment-plugin.customer",
330
+ inversedBy: "subscriptions"
331
+ }
332
+ };
333
+ const schema = {
334
+ kind,
335
+ collectionName,
336
+ info,
337
+ options,
338
+ attributes
339
+ };
340
+ const subscription = { schema };
239
341
  const contentTypes = {
240
342
  customer,
241
343
  order,
242
- payment
344
+ payment,
345
+ subscription
243
346
  };
244
347
  const controller = ({ strapi: strapi2 }) => ({
245
348
  index(ctx) {
246
349
  ctx.body = strapi2.plugin("payment-plugin").service("service").getWelcomeMessage();
247
350
  }
248
351
  });
352
+ const getErrorMessage = (error) => error instanceof Error ? error.message : String(error);
249
353
  const stripeController = {
250
354
  /**
251
355
  * Create a payment intent
@@ -416,12 +520,13 @@ const stripeController = {
416
520
  await stripeService2.handleWebhook(event);
417
521
  ctx.body = { received: true };
418
522
  } catch (error) {
419
- if (error.message.includes("No signatures found matching") || error.message.includes("Webhook payload must be provided as a string or a Buffer")) {
523
+ const message = getErrorMessage(error);
524
+ if (message.includes("No signatures found matching") || message.includes("Webhook payload must be provided as a string or a Buffer")) {
420
525
  strapi.log.error('Stripe Webhook Error: Payload is not raw. Ensure "includeUnparsed: true" is set in config/middlewares.ts for "strapi::body".');
421
526
  return ctx.badRequest("Webhook verification failed: Payload must be raw. Check your Strapi configuration for strapi::body middleware.");
422
527
  }
423
- strapi.log.error("Failed to handle webhook", { error: error.message, stack: error.stack });
424
- ctx.badRequest(`Webhook Error: ${error.message}`);
528
+ strapi.log.error("Failed to handle webhook", { message, stack: error instanceof Error ? error.stack : void 0 });
529
+ ctx.badRequest(`Webhook Error: ${message}`);
425
530
  }
426
531
  },
427
532
  /**
@@ -779,24 +884,28 @@ const stripeController = {
779
884
  const { startDate, endDate, format = "json" } = ctx.query;
780
885
  const filters = {};
781
886
  if (startDate || endDate) {
782
- filters.createdAt = {};
783
- if (startDate) filters.createdAt.$gte = new Date(startDate).toISOString();
784
- if (endDate) filters.createdAt.$lte = new Date(endDate).toISOString();
887
+ const dateFilter = {};
888
+ if (startDate) dateFilter.$gte = new Date(startDate).toISOString();
889
+ if (endDate) dateFilter.$lte = new Date(endDate).toISOString();
890
+ filters.createdAt = dateFilter;
785
891
  }
786
- const payments = await strapi.documents("plugin::payment-plugin.payment").findMany({
892
+ const rawPayments = await strapi.documents("plugin::payment-plugin.payment").findMany({
787
893
  filters,
788
894
  populate: ["customer", "order"],
789
895
  sort: { createdAt: "desc" }
790
896
  });
897
+ const payments = rawPayments;
791
898
  const summary = {
792
899
  totalPayments: payments.length,
793
900
  totalAmount: payments.reduce((sum, p) => sum + p.amount, 0),
794
901
  byStatus: payments.reduce((acc, p) => {
795
- acc[p.payment_status] = (acc[p.payment_status] || 0) + 1;
902
+ const s = p.payment_status || "unknown";
903
+ acc[s] = (acc[s] || 0) + 1;
796
904
  return acc;
797
905
  }, {}),
798
906
  byCurrency: payments.reduce((acc, p) => {
799
- acc[p.currency] = (acc[p.currency] || 0) + 1;
907
+ const c = p.currency || "unknown";
908
+ acc[c] = (acc[c] || 0) + 1;
800
909
  return acc;
801
910
  }, {})
802
911
  };
@@ -887,8 +996,9 @@ const stripeController = {
887
996
  fields: ["amount", "createdAt", "currency"],
888
997
  sort: { createdAt: "asc" }
889
998
  });
890
- const dailyStats = payments.reduce((acc, payment2) => {
891
- const date = new Date(payment2.createdAt).toISOString().split("T")[0];
999
+ const typedPayments = payments;
1000
+ const dailyStats = typedPayments.reduce((acc, payment2) => {
1001
+ const date = new Date(payment2.createdAt || "").toISOString().split("T")[0];
892
1002
  if (!acc[date]) {
893
1003
  acc[date] = { count: 0, amount: 0, currency: payment2.currency };
894
1004
  }
@@ -896,14 +1006,15 @@ const stripeController = {
896
1006
  acc[date].amount += payment2.amount;
897
1007
  return acc;
898
1008
  }, {});
1009
+ const totalVolume = typedPayments.reduce((sum, p) => sum + p.amount, 0);
899
1010
  ctx.body = {
900
1011
  success: true,
901
1012
  data: {
902
1013
  period,
903
1014
  dailyStats,
904
- totalVolume: payments.reduce((sum, p) => sum + p.amount, 0),
905
- totalTransactions: payments.length,
906
- averageTransaction: payments.length > 0 ? payments.reduce((sum, p) => sum + p.amount, 0) / payments.length : 0
1015
+ totalVolume,
1016
+ totalTransactions: typedPayments.length,
1017
+ averageTransaction: typedPayments.length > 0 ? totalVolume / typedPayments.length : 0
907
1018
  }
908
1019
  };
909
1020
  } catch (error) {
@@ -965,12 +1076,11 @@ const stripeController = {
965
1076
  };
966
1077
  } catch (error) {
967
1078
  strapi.log.error("Failed to create admin refund:", {
968
- message: error.message,
969
- stack: error.stack,
970
- ...error.raw && { stripeError: error.raw }
1079
+ message: getErrorMessage(error),
1080
+ stack: error instanceof Error ? error.stack : void 0,
1081
+ ...error instanceof Error && "raw" in error && { stripeError: error.raw }
971
1082
  });
972
- const errorMessage = error.message || "Failed to create admin refund";
973
- ctx.internalServerError(errorMessage);
1083
+ ctx.internalServerError(getErrorMessage(error) || "Failed to create admin refund");
974
1084
  }
975
1085
  },
976
1086
  /**
@@ -1222,9 +1332,207 @@ const stripeController = {
1222
1332
  }
1223
1333
  }
1224
1334
  };
1335
+ const subscriptionController = {
1336
+ /**
1337
+ * POST /subscriptions
1338
+ * Body: { stripeCustomerId, priceId, trialPeriodDays?, metadata?, defaultPaymentMethod? }
1339
+ */
1340
+ async createSubscription(ctx) {
1341
+ try {
1342
+ const { stripeCustomerId, priceId, trialPeriodDays, metadata, defaultPaymentMethod } = ctx.request.body;
1343
+ if (!stripeCustomerId || !priceId) {
1344
+ return ctx.badRequest("stripeCustomerId and priceId are required");
1345
+ }
1346
+ const stripeService2 = strapi.plugin("payment-plugin").service("stripe");
1347
+ const customers = await strapi.documents("plugin::payment-plugin.customer").findMany({
1348
+ filters: { stripe_customer_id: { $eq: stripeCustomerId } }
1349
+ });
1350
+ const strapiCustomerId = customers.length > 0 ? customers[0].documentId : void 0;
1351
+ const subscription2 = await stripeService2.createSubscription({
1352
+ strapiCustomerId: strapiCustomerId || "",
1353
+ stripeCustomerId,
1354
+ priceId,
1355
+ trialPeriodDays,
1356
+ metadata,
1357
+ defaultPaymentMethod
1358
+ });
1359
+ const invoice = typeof subscription2.latest_invoice === "object" ? subscription2.latest_invoice : null;
1360
+ const invoiceExt = invoice;
1361
+ const paymentIntent = typeof invoiceExt?.payment_intent === "object" ? invoiceExt.payment_intent : null;
1362
+ const setupIntent = typeof subscription2.pending_setup_intent === "object" ? subscription2.pending_setup_intent : null;
1363
+ ctx.body = {
1364
+ subscriptionId: subscription2.id,
1365
+ status: subscription2.status,
1366
+ clientSecret: paymentIntent?.client_secret || setupIntent?.client_secret || null,
1367
+ currentPeriodEnd: subscription2.current_period_end
1368
+ };
1369
+ } catch (error) {
1370
+ strapi.log.error("Failed to create subscription", { error: getErrorMessage(error) });
1371
+ ctx.internalServerError(getErrorMessage(error) || "Failed to create subscription");
1372
+ }
1373
+ },
1374
+ /**
1375
+ * POST /subscriptions/:id/cancel
1376
+ * Body: { cancelAtPeriodEnd? }
1377
+ */
1378
+ async cancelSubscription(ctx) {
1379
+ try {
1380
+ const { id } = ctx.params;
1381
+ const { cancelAtPeriodEnd = false } = ctx.request.body || {};
1382
+ const stripeService2 = strapi.plugin("payment-plugin").service("stripe");
1383
+ const subscription2 = await stripeService2.cancelSubscription(id, cancelAtPeriodEnd);
1384
+ ctx.body = {
1385
+ subscriptionId: subscription2.id,
1386
+ status: subscription2.status,
1387
+ cancelAtPeriodEnd: subscription2.cancel_at_period_end,
1388
+ canceledAt: subscription2.canceled_at
1389
+ };
1390
+ } catch (error) {
1391
+ strapi.log.error("Failed to cancel subscription", { error: getErrorMessage(error) });
1392
+ ctx.internalServerError(getErrorMessage(error) || "Failed to cancel subscription");
1393
+ }
1394
+ },
1395
+ /**
1396
+ * PUT /subscriptions/:id
1397
+ * Body: { priceId?, cancelAtPeriodEnd?, trialEnd?, metadata?, defaultPaymentMethod? }
1398
+ */
1399
+ async updateSubscription(ctx) {
1400
+ try {
1401
+ const { id } = ctx.params;
1402
+ const params = ctx.request.body || {};
1403
+ const stripeService2 = strapi.plugin("payment-plugin").service("stripe");
1404
+ const subscription2 = await stripeService2.updateSubscription(id, params);
1405
+ ctx.body = {
1406
+ subscriptionId: subscription2.id,
1407
+ status: subscription2.status,
1408
+ cancelAtPeriodEnd: subscription2.cancel_at_period_end,
1409
+ currentPeriodEnd: subscription2.current_period_end
1410
+ };
1411
+ } catch (error) {
1412
+ strapi.log.error("Failed to update subscription", { error: getErrorMessage(error) });
1413
+ ctx.internalServerError(getErrorMessage(error) || "Failed to update subscription");
1414
+ }
1415
+ },
1416
+ /**
1417
+ * GET /subscriptions/:id
1418
+ */
1419
+ async getSubscriptionDetails(ctx) {
1420
+ try {
1421
+ const { id } = ctx.params;
1422
+ const records = await strapi.documents("plugin::payment-plugin.subscription").findMany({
1423
+ filters: { stripe_subscription_id: { $eq: id } },
1424
+ populate: ["customer"]
1425
+ });
1426
+ if (records.length === 0) {
1427
+ return ctx.notFound("Subscription not found");
1428
+ }
1429
+ const stripeService2 = strapi.plugin("payment-plugin").service("stripe");
1430
+ const stripeSubscription = await stripeService2.retrieveSubscription(id);
1431
+ ctx.body = {
1432
+ strapi: records[0],
1433
+ stripe: stripeSubscription
1434
+ };
1435
+ } catch (error) {
1436
+ strapi.log.error("Failed to get subscription", { error: getErrorMessage(error) });
1437
+ ctx.internalServerError(getErrorMessage(error) || "Failed to get subscription");
1438
+ }
1439
+ },
1440
+ /**
1441
+ * GET /subscriptions
1442
+ * Query: page, pageSize, status, stripeCustomerId
1443
+ */
1444
+ async listSubscriptions(ctx) {
1445
+ try {
1446
+ const { page = 1, pageSize = 25, status, stripeCustomerId } = ctx.query;
1447
+ const filters = {};
1448
+ if (status) filters.status = { $eq: status };
1449
+ if (stripeCustomerId) filters.stripe_customer_id = { $eq: stripeCustomerId };
1450
+ const subscriptions = await strapi.documents("plugin::payment-plugin.subscription").findMany({
1451
+ filters,
1452
+ populate: ["customer"],
1453
+ sort: { createdAt: "desc" },
1454
+ start: (Number(page) - 1) * Number(pageSize),
1455
+ limit: Number(pageSize)
1456
+ });
1457
+ const total = await strapi.documents("plugin::payment-plugin.subscription").count({ filters });
1458
+ ctx.body = {
1459
+ data: subscriptions,
1460
+ meta: {
1461
+ pagination: {
1462
+ page: Number(page),
1463
+ pageSize: Number(pageSize),
1464
+ total,
1465
+ pageCount: Math.ceil(total / Number(pageSize))
1466
+ }
1467
+ }
1468
+ };
1469
+ } catch (error) {
1470
+ strapi.log.error("Failed to list subscriptions", { error: getErrorMessage(error) });
1471
+ ctx.internalServerError(getErrorMessage(error) || "Failed to list subscriptions");
1472
+ }
1473
+ },
1474
+ /**
1475
+ * GET /admin/subscriptions (admin only)
1476
+ */
1477
+ async adminListSubscriptions(ctx) {
1478
+ try {
1479
+ const { page = 1, pageSize = 25, status } = ctx.query;
1480
+ const filters = {};
1481
+ if (status) filters.status = { $eq: status };
1482
+ const subscriptions = await strapi.documents("plugin::payment-plugin.subscription").findMany({
1483
+ filters,
1484
+ populate: ["customer"],
1485
+ sort: { createdAt: "desc" },
1486
+ start: (Number(page) - 1) * Number(pageSize),
1487
+ limit: Number(pageSize)
1488
+ });
1489
+ const total = await strapi.documents("plugin::payment-plugin.subscription").count({ filters });
1490
+ const enriched = subscriptions.map((s) => ({
1491
+ ...s,
1492
+ customerEmail: s.customer?.email || "",
1493
+ customerName: s.customer ? `${s.customer.first_name} ${s.customer.last_name}` : ""
1494
+ }));
1495
+ ctx.body = {
1496
+ data: enriched,
1497
+ meta: {
1498
+ pagination: {
1499
+ page: Number(page),
1500
+ pageSize: Number(pageSize),
1501
+ total,
1502
+ pageCount: Math.ceil(total / Number(pageSize))
1503
+ }
1504
+ }
1505
+ };
1506
+ } catch (error) {
1507
+ strapi.log.error("Failed to list subscriptions (admin)", { error: getErrorMessage(error) });
1508
+ ctx.internalServerError(getErrorMessage(error) || "Failed to list subscriptions");
1509
+ }
1510
+ },
1511
+ /**
1512
+ * POST /admin/subscriptions/:id/cancel (admin only)
1513
+ */
1514
+ async adminCancelSubscription(ctx) {
1515
+ try {
1516
+ const { id } = ctx.params;
1517
+ const { cancelAtPeriodEnd = false } = ctx.request.body || {};
1518
+ const stripeService2 = strapi.plugin("payment-plugin").service("stripe");
1519
+ const subscription2 = await stripeService2.cancelSubscription(id, cancelAtPeriodEnd);
1520
+ ctx.body = {
1521
+ success: true,
1522
+ subscriptionId: subscription2.id,
1523
+ status: subscription2.status,
1524
+ cancelAtPeriodEnd: subscription2.cancel_at_period_end
1525
+ };
1526
+ } catch (error) {
1527
+ strapi.log.error("Failed to cancel subscription (admin)", { error: getErrorMessage(error) });
1528
+ ctx.internalServerError(getErrorMessage(error) || "Failed to cancel subscription");
1529
+ }
1530
+ }
1531
+ };
1532
+ const stripeControllerFull = { ...stripeController, ...subscriptionController };
1225
1533
  const controllers = {
1226
1534
  controller,
1227
- stripe: stripeController
1535
+ stripe: stripeControllerFull
1228
1536
  };
1229
1537
  const middlewares = {
1230
1538
  /**
@@ -1423,6 +1731,37 @@ const routes$2 = {
1423
1731
  config: {
1424
1732
  policies: []
1425
1733
  }
1734
+ },
1735
+ // Subscription Operations
1736
+ {
1737
+ method: "POST",
1738
+ path: "/subscriptions",
1739
+ handler: "stripe.createSubscription",
1740
+ config: { policies: [] }
1741
+ },
1742
+ {
1743
+ method: "GET",
1744
+ path: "/subscriptions",
1745
+ handler: "stripe.listSubscriptions",
1746
+ config: { policies: [] }
1747
+ },
1748
+ {
1749
+ method: "GET",
1750
+ path: "/subscriptions/:id",
1751
+ handler: "stripe.getSubscriptionDetails",
1752
+ config: { policies: [] }
1753
+ },
1754
+ {
1755
+ method: "PUT",
1756
+ path: "/subscriptions/:id",
1757
+ handler: "stripe.updateSubscription",
1758
+ config: { policies: [] }
1759
+ },
1760
+ {
1761
+ method: "POST",
1762
+ path: "/subscriptions/:id/cancel",
1763
+ handler: "stripe.cancelSubscription",
1764
+ config: { policies: [] }
1426
1765
  }
1427
1766
  ]
1428
1767
  };
@@ -1546,6 +1885,25 @@ const routes$1 = {
1546
1885
  policies: ["admin::isAuthenticatedAdmin"],
1547
1886
  middlewares: []
1548
1887
  }
1888
+ },
1889
+ // Admin Subscription Management
1890
+ {
1891
+ method: "GET",
1892
+ path: "/admin/subscriptions",
1893
+ handler: "stripe.adminListSubscriptions",
1894
+ config: {
1895
+ policies: ["admin::isAuthenticatedAdmin"],
1896
+ middlewares: []
1897
+ }
1898
+ },
1899
+ {
1900
+ method: "POST",
1901
+ path: "/admin/subscriptions/:id/cancel",
1902
+ handler: "stripe.adminCancelSubscription",
1903
+ config: {
1904
+ policies: ["admin::isAuthenticatedAdmin"],
1905
+ middlewares: []
1906
+ }
1549
1907
  }
1550
1908
  ]
1551
1909
  };
@@ -1592,6 +1950,9 @@ const stripeService = ({ strapi: strapi2 }) => {
1592
1950
  const getStripe = () => {
1593
1951
  return stripe;
1594
1952
  };
1953
+ const setStripe = (instance) => {
1954
+ stripe = instance;
1955
+ };
1595
1956
  const mapStripeStatusToStrapi = (stripeStatus) => {
1596
1957
  switch (stripeStatus) {
1597
1958
  case "succeeded":
@@ -1749,14 +2110,14 @@ const stripeService = ({ strapi: strapi2 }) => {
1749
2110
  return refund;
1750
2111
  } catch (error) {
1751
2112
  const logger = getLogger();
1752
- const errorMessage = error instanceof Error ? error.message : "Unknown Stripe error";
2113
+ const errorMessage = getErrorMessage(error);
1753
2114
  logger.error("Failed to create Stripe refund", {
1754
2115
  message: errorMessage,
1755
2116
  error: error instanceof Error ? {
1756
2117
  message: error.message,
1757
2118
  stack: error.stack,
1758
- ...error.raw && { raw: error.raw }
1759
- } : error,
2119
+ ..."raw" in error && { raw: error.raw }
2120
+ } : String(error),
1760
2121
  params
1761
2122
  });
1762
2123
  throw error;
@@ -1779,8 +2140,8 @@ const stripeService = ({ strapi: strapi2 }) => {
1779
2140
  return stripe2.webhooks.constructEvent(verifiedPayload, signature, webhookSecret);
1780
2141
  } catch (error) {
1781
2142
  logger.error("Failed to construct webhook event", {
1782
- message: error.message,
1783
- type: error.type
2143
+ message: getErrorMessage(error),
2144
+ ...error instanceof Error && "type" in error && { type: error.type }
1784
2145
  });
1785
2146
  throw error;
1786
2147
  }
@@ -1853,10 +2214,7 @@ const stripeService = ({ strapi: strapi2 }) => {
1853
2214
  };
1854
2215
  const createOrderRecord = async (data) => {
1855
2216
  return strapi2.documents("plugin::payment-plugin.order").create({
1856
- data: {
1857
- ...data,
1858
- order_status: data.order_status || "pending"
1859
- }
2217
+ data: { ...data, order_status: data.order_status || "pending" }
1860
2218
  });
1861
2219
  };
1862
2220
  const createStrapiPaymentRecord = async (paymentIntent) => {
@@ -1973,10 +2331,18 @@ const stripeService = ({ strapi: strapi2 }) => {
1973
2331
  switch (event.type) {
1974
2332
  case "payment_intent.succeeded":
1975
2333
  case "charge.succeeded": {
1976
- const obj = event.data.object;
1977
- const paymentIntentId = event.type === "payment_intent.succeeded" ? obj.id : obj.payment_intent;
2334
+ let paymentIntentId;
2335
+ let metadata;
2336
+ if (event.type === "payment_intent.succeeded") {
2337
+ const piObj = event.data.object;
2338
+ paymentIntentId = piObj.id;
2339
+ metadata = piObj.metadata || {};
2340
+ } else {
2341
+ const chargeObj = event.data.object;
2342
+ paymentIntentId = chargeObj.payment_intent;
2343
+ metadata = chargeObj.metadata || {};
2344
+ }
1978
2345
  if (!paymentIntentId) break;
1979
- const metadata = obj.metadata || {};
1980
2346
  await updateStrapiPayment(paymentIntentId, {
1981
2347
  payment_status: "succeeded",
1982
2348
  metadata
@@ -1984,12 +2350,14 @@ const stripeService = ({ strapi: strapi2 }) => {
1984
2350
  const existing = await strapi2.documents("plugin::payment-plugin.payment").findMany({
1985
2351
  filters: { stripe_payment_intent_id: paymentIntentId }
1986
2352
  });
1987
- if (existing.length === 0 && event.type === "payment_intent.succeeded") {
1988
- await createStrapiPaymentRecord(obj);
1989
- } else if (existing.length === 0 && event.type === "charge.succeeded" && obj.payment_intent) {
1990
- const stripe2 = await initializeStripe();
1991
- const pi = await stripe2.paymentIntents.retrieve(obj.payment_intent);
1992
- await createStrapiPaymentRecord(pi);
2353
+ if (existing.length === 0) {
2354
+ if (event.type === "payment_intent.succeeded") {
2355
+ await createStrapiPaymentRecord(event.data.object);
2356
+ } else {
2357
+ const stripeClient = await initializeStripe();
2358
+ const pi = await stripeClient.paymentIntents.retrieve(paymentIntentId);
2359
+ await createStrapiPaymentRecord(pi);
2360
+ }
1993
2361
  }
1994
2362
  const orderId = metadata.strapi_order_id;
1995
2363
  if (orderId) {
@@ -2096,6 +2464,73 @@ const stripeService = ({ strapi: strapi2 }) => {
2096
2464
  });
2097
2465
  break;
2098
2466
  }
2467
+ case "customer.subscription.created": {
2468
+ const subscription2 = event.data.object;
2469
+ const strapiCustomerId = subscription2.metadata?.strapi_customer_id;
2470
+ const existing = await strapi2.documents("plugin::payment-plugin.subscription").findMany({
2471
+ filters: { stripe_subscription_id: subscription2.id }
2472
+ });
2473
+ if (existing.length === 0) {
2474
+ await createStrapiSubscriptionRecord(subscription2, strapiCustomerId);
2475
+ }
2476
+ logger.info("Subscription created via webhook", { subscriptionId: subscription2.id });
2477
+ break;
2478
+ }
2479
+ case "customer.subscription.updated": {
2480
+ const subscription2 = event.data.object;
2481
+ const sub = subscription2;
2482
+ const firstItem = subscription2.items.data[0];
2483
+ await updateStrapiSubscription(subscription2.id, {
2484
+ status: mapStripeSubscriptionStatus(subscription2.status),
2485
+ stripe_price_id: firstItem?.price?.id,
2486
+ stripe_product_id: firstItem?.price?.product,
2487
+ current_period_start: toISOOrNull(sub.current_period_start),
2488
+ current_period_end: toISOOrNull(sub.current_period_end),
2489
+ cancel_at_period_end: subscription2.cancel_at_period_end,
2490
+ canceled_at: toISOOrNull(subscription2.canceled_at),
2491
+ trial_start: toISOOrNull(subscription2.trial_start),
2492
+ trial_end: toISOOrNull(subscription2.trial_end),
2493
+ latest_invoice_id: typeof subscription2.latest_invoice === "string" ? subscription2.latest_invoice : subscription2.latest_invoice?.id,
2494
+ metadata: subscription2.metadata
2495
+ });
2496
+ logger.info("Subscription updated via webhook", { subscriptionId: subscription2.id, status: subscription2.status });
2497
+ break;
2498
+ }
2499
+ case "customer.subscription.deleted": {
2500
+ const subscription2 = event.data.object;
2501
+ await updateStrapiSubscription(subscription2.id, {
2502
+ status: "canceled",
2503
+ canceled_at: toISOOrNull(subscription2.canceled_at) || (/* @__PURE__ */ new Date()).toISOString()
2504
+ });
2505
+ logger.info("Subscription deleted via webhook", { subscriptionId: subscription2.id });
2506
+ break;
2507
+ }
2508
+ case "invoice.payment_succeeded": {
2509
+ const invoice = event.data.object;
2510
+ const subDetails = invoice.parent?.subscription_details;
2511
+ const subscriptionId = typeof subDetails?.subscription === "string" ? subDetails.subscription : subDetails?.subscription?.id ?? null;
2512
+ if (subscriptionId) {
2513
+ await updateStrapiSubscription(subscriptionId, {
2514
+ status: "active",
2515
+ latest_invoice_id: invoice.id
2516
+ });
2517
+ }
2518
+ logger.info("Invoice payment succeeded", { invoiceId: invoice.id, subscriptionId });
2519
+ break;
2520
+ }
2521
+ case "invoice.payment_failed": {
2522
+ const invoice = event.data.object;
2523
+ const subDetails = invoice.parent?.subscription_details;
2524
+ const subscriptionId = typeof subDetails?.subscription === "string" ? subDetails.subscription : subDetails?.subscription?.id ?? null;
2525
+ if (subscriptionId) {
2526
+ await updateStrapiSubscription(subscriptionId, {
2527
+ status: "past_due",
2528
+ latest_invoice_id: invoice.id
2529
+ });
2530
+ }
2531
+ logger.warn("Invoice payment failed", { invoiceId: invoice.id, subscriptionId });
2532
+ break;
2533
+ }
2099
2534
  default:
2100
2535
  logger.info("Unhandled webhook event type", { eventType: event.type });
2101
2536
  }
@@ -2108,6 +2543,140 @@ const stripeService = ({ strapi: strapi2 }) => {
2108
2543
  throw error;
2109
2544
  }
2110
2545
  };
2546
+ const mapStripeSubscriptionStatus = (status) => {
2547
+ const valid = ["active", "canceled", "incomplete", "incomplete_expired", "past_due", "paused", "trialing", "unpaid"];
2548
+ return valid.includes(status) ? status : "incomplete";
2549
+ };
2550
+ const toISOOrNull = (ts) => {
2551
+ if (!ts) return null;
2552
+ return new Date(ts * 1e3).toISOString();
2553
+ };
2554
+ const createStrapiSubscriptionRecord = async (subscription2, strapiCustomerId) => {
2555
+ const logger = getLogger();
2556
+ try {
2557
+ const firstItem = subscription2.items.data[0];
2558
+ const price = firstItem?.price;
2559
+ const data = {
2560
+ stripe_subscription_id: subscription2.id,
2561
+ stripe_customer_id: subscription2.customer,
2562
+ stripe_price_id: price?.id || "",
2563
+ stripe_product_id: price?.product,
2564
+ status: mapStripeSubscriptionStatus(subscription2.status),
2565
+ current_period_start: toISOOrNull(subscription2.current_period_start) || void 0,
2566
+ current_period_end: toISOOrNull(subscription2.current_period_end) || void 0,
2567
+ cancel_at_period_end: subscription2.cancel_at_period_end,
2568
+ canceled_at: toISOOrNull(subscription2.canceled_at) || void 0,
2569
+ trial_start: toISOOrNull(subscription2.trial_start) || void 0,
2570
+ trial_end: toISOOrNull(subscription2.trial_end) || void 0,
2571
+ latest_invoice_id: typeof subscription2.latest_invoice === "string" ? subscription2.latest_invoice : subscription2.latest_invoice?.id,
2572
+ metadata: subscription2.metadata,
2573
+ customer: strapiCustomerId
2574
+ };
2575
+ const record = await strapi2.documents("plugin::payment-plugin.subscription").create({ data });
2576
+ logger.info("Created Strapi subscription record", {
2577
+ stripeSubscriptionId: subscription2.id,
2578
+ strapiCustomerId
2579
+ });
2580
+ return record;
2581
+ } catch (error) {
2582
+ logger.error("Failed to create Strapi subscription record", { error, subscriptionId: subscription2.id });
2583
+ throw error;
2584
+ }
2585
+ };
2586
+ const updateStrapiSubscription = async (stripeSubscriptionId, data) => {
2587
+ const logger = getLogger();
2588
+ try {
2589
+ const existing = await strapi2.documents("plugin::payment-plugin.subscription").findMany({
2590
+ filters: { stripe_subscription_id: stripeSubscriptionId }
2591
+ });
2592
+ if (existing.length > 0) {
2593
+ await strapi2.documents("plugin::payment-plugin.subscription").update({
2594
+ documentId: existing[0].documentId,
2595
+ data
2596
+ });
2597
+ }
2598
+ } catch (error) {
2599
+ logger.error("Failed to update Strapi subscription record", { error, stripeSubscriptionId });
2600
+ }
2601
+ };
2602
+ const createSubscription = async (params) => {
2603
+ const stripe2 = await initializeStripe();
2604
+ const { strapiCustomerId, stripeCustomerId, priceId, trialPeriodDays, metadata = {}, defaultPaymentMethod } = params;
2605
+ const subscriptionParams = {
2606
+ customer: stripeCustomerId,
2607
+ items: [{ price: priceId }],
2608
+ payment_behavior: "default_incomplete",
2609
+ payment_settings: { save_default_payment_method: "on_subscription" },
2610
+ expand: ["latest_invoice.payment_intent", "pending_setup_intent"],
2611
+ metadata: { ...metadata, strapi_customer_id: strapiCustomerId }
2612
+ };
2613
+ if (trialPeriodDays) {
2614
+ subscriptionParams.trial_period_days = trialPeriodDays;
2615
+ }
2616
+ if (defaultPaymentMethod) {
2617
+ subscriptionParams.default_payment_method = defaultPaymentMethod;
2618
+ }
2619
+ const subscription2 = await stripe2.subscriptions.create(subscriptionParams);
2620
+ await createStrapiSubscriptionRecord(subscription2, strapiCustomerId);
2621
+ getLogger().info("Subscription created", { subscriptionId: subscription2.id, stripeCustomerId });
2622
+ return subscription2;
2623
+ };
2624
+ const cancelSubscription = async (stripeSubscriptionId, cancelAtPeriodEnd = false) => {
2625
+ const stripe2 = await initializeStripe();
2626
+ let subscription2;
2627
+ if (cancelAtPeriodEnd) {
2628
+ subscription2 = await stripe2.subscriptions.update(stripeSubscriptionId, { cancel_at_period_end: true });
2629
+ await updateStrapiSubscription(stripeSubscriptionId, { cancel_at_period_end: true });
2630
+ } else {
2631
+ subscription2 = await stripe2.subscriptions.cancel(stripeSubscriptionId);
2632
+ await updateStrapiSubscription(stripeSubscriptionId, {
2633
+ status: "canceled",
2634
+ canceled_at: (/* @__PURE__ */ new Date()).toISOString(),
2635
+ cancel_at_period_end: false
2636
+ });
2637
+ }
2638
+ getLogger().info("Subscription canceled", { stripeSubscriptionId, cancelAtPeriodEnd });
2639
+ return subscription2;
2640
+ };
2641
+ const updateSubscription = async (stripeSubscriptionId, params) => {
2642
+ const stripe2 = await initializeStripe();
2643
+ const updateParams = {};
2644
+ if (params.priceId) {
2645
+ const current = await stripe2.subscriptions.retrieve(stripeSubscriptionId);
2646
+ const itemId = current.items.data[0]?.id;
2647
+ updateParams.items = [{ id: itemId, price: params.priceId }];
2648
+ updateParams.proration_behavior = "create_prorations";
2649
+ }
2650
+ if (params.cancelAtPeriodEnd !== void 0) {
2651
+ updateParams.cancel_at_period_end = params.cancelAtPeriodEnd;
2652
+ }
2653
+ if (params.trialEnd !== void 0) {
2654
+ updateParams.trial_end = params.trialEnd;
2655
+ }
2656
+ if (params.metadata) {
2657
+ updateParams.metadata = params.metadata;
2658
+ }
2659
+ if (params.defaultPaymentMethod) {
2660
+ updateParams.default_payment_method = params.defaultPaymentMethod;
2661
+ }
2662
+ const subscription2 = await stripe2.subscriptions.update(stripeSubscriptionId, updateParams);
2663
+ const firstItem = subscription2.items.data[0];
2664
+ await updateStrapiSubscription(stripeSubscriptionId, {
2665
+ stripe_price_id: firstItem?.price?.id,
2666
+ stripe_product_id: firstItem?.price?.product,
2667
+ cancel_at_period_end: subscription2.cancel_at_period_end,
2668
+ status: mapStripeSubscriptionStatus(subscription2.status),
2669
+ metadata: subscription2.metadata
2670
+ });
2671
+ getLogger().info("Subscription updated", { stripeSubscriptionId });
2672
+ return subscription2;
2673
+ };
2674
+ const retrieveSubscription = async (stripeSubscriptionId) => {
2675
+ const stripe2 = await initializeStripe();
2676
+ return stripe2.subscriptions.retrieve(stripeSubscriptionId, {
2677
+ expand: ["latest_invoice", "customer"]
2678
+ });
2679
+ };
2111
2680
  return {
2112
2681
  initializeStripe,
2113
2682
  createPaymentIntent,
@@ -2123,7 +2692,14 @@ const stripeService = ({ strapi: strapi2 }) => {
2123
2692
  createCustomerRecord,
2124
2693
  createOrderRecord,
2125
2694
  initializePaymentFlow,
2126
- getStripe
2695
+ getStripe,
2696
+ setStripe,
2697
+ createSubscription,
2698
+ cancelSubscription,
2699
+ updateSubscription,
2700
+ retrieveSubscription,
2701
+ createStrapiSubscriptionRecord,
2702
+ updateStrapiSubscription
2127
2703
  };
2128
2704
  };
2129
2705
  const services = {