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