From dd2c4ad91e234740e2465826942a983eeb52512f Mon Sep 17 00:00:00 2001 From: Piyush Singh Gaur Date: Wed, 25 Mar 2026 09:41:19 +0530 Subject: [PATCH 1/7] feat(provider): implement a comprehensive subscription lifecycle in stripe Implement a comprehensive subscription lifecycle within loopback4-billing to support automatedrecurring billing, including subscription creation, upgrades and downgrades, renewals,cancellations, and proration, ensuring consistency and scalability for SaaS monetization --- package-lock.json | 45 +- package.json | 4 +- .../unit/stripe-subscription.service.unit.ts | 643 ++++++++++++++++++ src/keys.ts | 11 +- src/providers/billing.provider.ts | 25 +- src/providers/sdk/stripe/adapter/index.ts | 1 + .../stripe/adapter/subscription.adapter.ts | 67 ++ src/providers/sdk/stripe/stripe.service.ts | 280 +++++++- src/providers/sdk/stripe/type/index.ts | 17 +- .../sdk/stripe/type/stripe-config.type.ts | 14 + src/types.ts | 168 +++++ 11 files changed, 1224 insertions(+), 51 deletions(-) create mode 100644 src/__tests__/unit/stripe-subscription.service.unit.ts create mode 100644 src/providers/sdk/stripe/adapter/subscription.adapter.ts create mode 100644 src/providers/sdk/stripe/type/stripe-config.type.ts diff --git a/package-lock.json b/package-lock.json index 2f3a2d2..9eec7b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1214,7 +1214,6 @@ "resolved": "https://registry.npmjs.org/@loopback/core/-/core-7.0.4.tgz", "integrity": "sha512-SjPTGa4T9DfQvRJ/drDfNpDjwKaOlpAMpTuaPBS83U6NjtLb6auOVIYJ3/nf+iZC58QAC8fZQnx45uWBgtQEUg==", "license": "MIT", - "peer": true, "dependencies": { "@loopback/context": "^8.0.4", "debug": "^4.4.1", @@ -1275,6 +1274,7 @@ "resolved": "https://registry.npmjs.org/@loopback/filter/-/filter-6.0.4.tgz", "integrity": "sha512-RjCdyIG9bKFbi4OWWOL1kH2c1vpF+o6jWVgh0J32h88rmQQpXE0qoDhilRK3Z880wRAizMv4V8UHB6hYLAIGhg==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.8.1" }, @@ -1376,7 +1376,6 @@ "resolved": "https://registry.npmjs.org/@loopback/rest/-/rest-15.0.5.tgz", "integrity": "sha512-gOr7xJ5SvDruyt955+H1UswANETRE7d5lyfWFZ7ETVsqJ3Yl3bKyGAJ7gR/twKO2WWtr5pe6wavC48zkMI/1og==", "license": "MIT", - "peer": true, "dependencies": { "@loopback/express": "^8.0.4", "@loopback/http-server": "^7.0.4", @@ -1527,7 +1526,6 @@ "integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.2", @@ -2602,7 +2600,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.129.tgz", "integrity": "sha512-hrmi5jWt2w60ayox3iIXwpMEnfUvOLJCRtrOPbHtH15nTjvO7uhnelvrdAs0dO0/zl5DZ3ZbahiaXEVb54ca/A==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~5.26.4" } @@ -2754,7 +2751,6 @@ "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/types": "7.18.0", @@ -2956,7 +2952,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3016,7 +3011,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3289,6 +3283,7 @@ "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", "license": "MIT", + "peer": true, "dependencies": { "buffer": "^6.0.3", "inherits": "^2.0.4", @@ -3314,6 +3309,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" @@ -3324,6 +3320,7 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "license": "MIT", + "peer": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -3337,7 +3334,8 @@ "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/body-parser": { "version": "2.2.0", @@ -3415,7 +3413,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -3560,6 +3557,7 @@ "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", "license": "MIT", + "peer": true, "dependencies": { "pascal-case": "^3.1.2", "tslib": "^2.0.3" @@ -3619,6 +3617,7 @@ "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==", "license": "MIT", + "peer": true, "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3", @@ -3647,6 +3646,7 @@ "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz", "integrity": "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==", "license": "MIT", + "peer": true, "dependencies": { "camel-case": "^4.1.2", "capital-case": "^1.0.4", @@ -4044,6 +4044,7 @@ "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz", "integrity": "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==", "license": "MIT", + "peer": true, "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3", @@ -4239,7 +4240,6 @@ "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", @@ -4998,6 +4998,7 @@ "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", "license": "MIT", + "peer": true, "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" @@ -5392,7 +5393,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -6928,6 +6928,7 @@ "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", "integrity": "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==", "license": "MIT", + "peer": true, "dependencies": { "capital-case": "^1.0.4", "tslib": "^2.0.3" @@ -7240,6 +7241,7 @@ "resolved": "https://registry.npmjs.org/inflection/-/inflection-3.0.2.tgz", "integrity": "sha512-+Bg3+kg+J6JUWn8J6bzFmOWkTQ6L/NHfDRSYU+EVvuKHDxUDHAXgqixHfVlzuBQaPOTac8hn43aPhMNk6rMe3g==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" } @@ -8196,6 +8198,7 @@ "resolved": "https://registry.npmjs.org/loopback-connector/-/loopback-connector-6.2.11.tgz", "integrity": "sha512-4jcFe64x7KNXTqp/vcBnl1M8wzPaFsL6RzVcCcZTUzGUEDdifi5Gc9VXu9Qbb0OcTNaE49KyeGG/LIXxuzV48A==", "license": "MIT", + "peer": true, "dependencies": { "async": "^3.2.6", "bluebird": "^3.7.2", @@ -8213,6 +8216,7 @@ "resolved": "https://registry.npmjs.org/loopback-datasource-juggler/-/loopback-datasource-juggler-5.2.1.tgz", "integrity": "sha512-AZr2i/bmlxJi9OM+9GdS0nPvbS6O/LNORqXE+IdQcjGDmMVKQZr2YLeNJiWU1kyHIHtQ7Q+LeNHyPAa8Usei8w==", "license": "MIT", + "peer": true, "dependencies": { "async": "^3.2.6", "change-case": "^4.1.2", @@ -8237,6 +8241,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", "license": "ISC", + "peer": true, "dependencies": { "@isaacs/brace-expansion": "^5.0.0" }, @@ -8252,6 +8257,7 @@ "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.0.3" } @@ -8333,7 +8339,6 @@ "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "dev": true, "license": "MIT", - "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -8797,6 +8802,7 @@ "resolved": "https://registry.npmjs.org/msgpack5/-/msgpack5-6.0.2.tgz", "integrity": "sha512-kBSpECAWslrciRF3jy6HkMckNa14j3VZwNUUe1ONO/yihs19MskiFnsWXm0Q0aPkDYDBRFvTKkEuEDY+HVxBvQ==", "license": "MIT", + "peer": true, "dependencies": { "bl": "^5.0.0", "inherits": "^2.0.3", @@ -8809,6 +8815,7 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "license": "MIT", + "peer": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -8848,6 +8855,7 @@ } ], "license": "MIT", + "peer": true, "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -8882,6 +8890,7 @@ "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.18.tgz", "integrity": "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==", "license": "MIT", + "peer": true, "engines": { "node": ">= 10" } @@ -8898,6 +8907,7 @@ "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", "license": "MIT", + "peer": true, "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" @@ -11425,7 +11435,6 @@ "dev": true, "inBundle": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -12450,6 +12459,7 @@ "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", "license": "MIT", + "peer": true, "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" @@ -12548,6 +12558,7 @@ "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", "license": "MIT", + "peer": true, "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" @@ -12558,6 +12569,7 @@ "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz", "integrity": "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==", "license": "MIT", + "peer": true, "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" @@ -13686,7 +13698,6 @@ "integrity": "sha512-phCkJ6pjDi9ANdhuF5ElS10GGdAKY6R1Pvt9lT3SFhOwM4T7QZE7MLpBDbNruUx/Q3gFD92/UOFringGipRqZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^13.0.0-beta.1", "@semantic-release/error": "^4.0.0", @@ -14292,6 +14303,7 @@ "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", "integrity": "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==", "license": "MIT", + "peer": true, "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3", @@ -14663,6 +14675,7 @@ "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", "license": "MIT", + "peer": true, "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" @@ -15544,7 +15557,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -15776,7 +15788,6 @@ "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15917,6 +15928,7 @@ "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz", "integrity": "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.0.3" } @@ -15926,6 +15938,7 @@ "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.0.3" } diff --git a/package.json b/package.json index 7f569c4..15266ba 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "eslint": "lb-eslint --report-unused-disable-directives .", "eslint:fix": "npm run eslint -- --fix", "pretest": "npm run build", - "test": "echo No Tests", + "test": "lb-mocha --allow-console-logs dist/__tests__/**/*.js", "posttest": "npm run lint", "test:dev": "lb-mocha --allow-console-logs dist/__tests__/**/*.js && npm run posttest", "prepublishOnly": "npm run test", @@ -120,4 +120,4 @@ ], "repositoryUrl": "git@github.com:sourcefuse/loopback4-billing.git" } -} +} \ No newline at end of file diff --git a/src/__tests__/unit/stripe-subscription.service.unit.ts b/src/__tests__/unit/stripe-subscription.service.unit.ts new file mode 100644 index 0000000..415af5c --- /dev/null +++ b/src/__tests__/unit/stripe-subscription.service.unit.ts @@ -0,0 +1,643 @@ +import {expect, sinon} from '@loopback/testlab'; +import {StripeService} from '../../providers/sdk/stripe/stripe.service'; +import { + CollectionMethod, + ProrationBehavior, + RecurringInterval, + TPrice, + TProduct, + TSubscriptionCreate, + TSubscriptionUpdate, +} from '../../types'; + +// --------------------------------------------------------------------------- +// Helper types used inside tests only +// --------------------------------------------------------------------------- + +interface StubStripeSubscriptionItem { + id: string; +} + +interface StubStripeSubscription { + id: string; + status: string; + customer: string; + cancel_at_period_end: boolean; + current_period_start: number; + current_period_end: number; + items: {data: StubStripeSubscriptionItem[]}; +} + +interface StubStripeInvoice { + id: string; + status: string; +} + +interface StubStripeInvoiceDetail { + currency: string; + total: number; + total_tax_amounts: {amount: number}[]; +} + +interface StubStripePrice { + id: string; + currency: string; + unit_amount: number | null; + product: string; + recurring: {interval: string; interval_count: number} | null; + metadata: Record; + active: boolean; +} + +interface StubbedStripe { + products: { + create: sinon.SinonStub; + retrieve: sinon.SinonStub; + }; + prices: { + create: sinon.SinonStub; + }; + subscriptions: { + create: sinon.SinonStub; + retrieve: sinon.SinonStub; + update: sinon.SinonStub; + cancel: sinon.SinonStub; + }; + invoices: { + retrieve: sinon.SinonStub; + list: sinon.SinonStub; + sendInvoice: sinon.SinonStub; + voidInvoice: sinon.SinonStub; + finalizeInvoice: sinon.SinonStub; + }; +} + +// --------------------------------------------------------------------------- +// Suite +// --------------------------------------------------------------------------- + +describe('StripeService - Subscription Management', () => { + let service: StripeService; + let sandbox: sinon.SinonSandbox; + let stripeStub: StubbedStripe; + + /** + * Create a fresh StripeService with all Stripe API calls stubbed out so no + * real network requests are made. + */ + beforeEach(() => { + sandbox = sinon.createSandbox(); + + // Instantiate with a dummy key (never reaches the network) + service = new StripeService({secretKey: 'sk_test_dummy'}); + + stripeStub = { + products: { + create: sandbox.stub(), + retrieve: sandbox.stub(), + }, + prices: { + create: sandbox.stub(), + }, + subscriptions: { + create: sandbox.stub(), + retrieve: sandbox.stub(), + update: sandbox.stub(), + cancel: sandbox.stub(), + }, + invoices: { + retrieve: sandbox.stub(), + list: sandbox.stub(), + sendInvoice: sandbox.stub(), + voidInvoice: sandbox.stub(), + finalizeInvoice: sandbox.stub(), + }, + }; + + // Override the protected stripe field so every SDK call hits a stub + (service as unknown as {stripe: StubbedStripe}).stripe = stripeStub; + }); + + afterEach(() => { + sandbox.restore(); + }); + + // ------------------------------------------------------------------------- + // createProduct + // ------------------------------------------------------------------------- + + describe('createProduct', () => { + it('creates a product and returns its Stripe ID', async () => { + const product: TProduct = { + name: 'Enterprise Plan', + description: 'Full-featured enterprise subscription', + metadata: {tier: 'enterprise'}, + }; + + stripeStub.products.create.resolves({id: 'prod_enterprise_123'}); + + const result = await service.createProduct(product); + + expect(result).to.equal('prod_enterprise_123'); + sinon.assert.calledOnceWithExactly(stripeStub.products.create, { + name: product.name, + description: product.description, + metadata: product.metadata, + }); + }); + }); + + // ------------------------------------------------------------------------- + // createPrice + // ------------------------------------------------------------------------- + + describe('createPrice', () => { + it('creates a recurring price and maps the response to TPrice', async () => { + const priceInput: TPrice = { + currency: 'usd', + unitAmount: 4999, + product: 'prod_enterprise_123', + recurring: { + interval: RecurringInterval.MONTH, + intervalCount: 1, + }, + metadata: {plan: 'monthly'}, + }; + + const stripeResponse: StubStripePrice = { + id: 'price_monthly_456', + currency: 'usd', + unit_amount: 4999, + product: 'prod_enterprise_123', + recurring: {interval: 'month', interval_count: 1}, + metadata: {plan: 'monthly'}, + active: true, + }; + + stripeStub.prices.create.resolves(stripeResponse); + + const result = await service.createPrice(priceInput); + + expect(result.id).to.equal('price_monthly_456'); + expect(result.currency).to.equal('usd'); + expect(result.unitAmount).to.equal(4999); + expect(result.product).to.equal('prod_enterprise_123'); + expect(result.recurring?.interval).to.equal(RecurringInterval.MONTH); + expect(result.recurring?.intervalCount).to.equal(1); + expect(result.active).to.be.true(); + }); + + it('handles a one-time price (no recurring field)', async () => { + const priceInput: TPrice = { + currency: 'usd', + unitAmount: 999, + product: 'prod_setup_fee', + }; + + const stripeResponse: StubStripePrice = { + id: 'price_setup_789', + currency: 'usd', + unit_amount: 999, + product: 'prod_setup_fee', + recurring: null, + metadata: {}, + active: true, + }; + + stripeStub.prices.create.resolves(stripeResponse); + + const result = await service.createPrice(priceInput); + + expect(result.recurring).to.be.undefined(); + }); + }); + + // ------------------------------------------------------------------------- + // createSubscription + // ------------------------------------------------------------------------- + + describe('createSubscription', () => { + it('creates a subscription and returns its Stripe ID', async () => { + const subscriptionInput: TSubscriptionCreate = { + customerId: 'cus_tenant_abc', + priceRefId: 'price_monthly_456', + collectionMethod: CollectionMethod.CHARGE_AUTOMATICALLY, + }; + + stripeStub.subscriptions.create.resolves({id: 'sub_new_001'}); + + const result = await service.createSubscription(subscriptionInput); + + expect(result).to.equal('sub_new_001'); + sinon.assert.calledOnce(stripeStub.subscriptions.create); + + const callArg = stripeStub.subscriptions.create.firstCall.args[0]; + expect(callArg.customer).to.equal('cus_tenant_abc'); + expect(callArg.payment_behavior).to.equal('default_incomplete'); + }); + + it('passes daysUntilDue when collection method is send_invoice', async () => { + const subscriptionInput: TSubscriptionCreate = { + customerId: 'cus_tenant_xyz', + priceRefId: 'price_monthly_456', + collectionMethod: CollectionMethod.SEND_INVOICE, + daysUntilDue: 30, + }; + + stripeStub.subscriptions.create.resolves({id: 'sub_invoice_002'}); + + await service.createSubscription(subscriptionInput); + + const callArg = stripeStub.subscriptions.create.firstCall.args[0]; + expect(callArg.days_until_due).to.equal(30); + expect(callArg.collection_method).to.equal(CollectionMethod.SEND_INVOICE); + }); + + it('uses defaultPaymentBehavior from StripeConfig when provided', async () => { + // Create a separate service instance with a custom payment behavior so + // callers are not forced to use the SCA-default 'default_incomplete'. + const customService = new StripeService({ + secretKey: 'sk_test_dummy', + defaultPaymentBehavior: 'allow_incomplete', + }); + (customService as unknown as {stripe: StubbedStripe}).stripe = stripeStub; + + stripeStub.subscriptions.create.resolves({id: 'sub_custom_003'}); + + await customService.createSubscription({ + customerId: 'cus_custom', + priceRefId: 'price_abc', + collectionMethod: CollectionMethod.CHARGE_AUTOMATICALLY, + }); + + const callArg = stripeStub.subscriptions.create.firstCall.args[0]; + expect(callArg.payment_behavior).to.equal('allow_incomplete'); + }); + + it('falls back to default_incomplete when defaultPaymentBehavior is not configured', async () => { + stripeStub.subscriptions.create.resolves({id: 'sub_default_004'}); + + await service.createSubscription({ + customerId: 'cus_fallback', + priceRefId: 'price_fallback', + collectionMethod: CollectionMethod.CHARGE_AUTOMATICALLY, + }); + + const callArg = stripeStub.subscriptions.create.firstCall.args[0]; + expect(callArg.payment_behavior).to.equal('default_incomplete'); + }); + }); + + // ------------------------------------------------------------------------- + // getSubscription + // ------------------------------------------------------------------------- + + describe('getSubscription', () => { + it('retrieves and maps a subscription to TSubscriptionResult', async () => { + const stubSub: StubStripeSubscription = { + id: 'sub_active_001', + status: 'active', + customer: 'cus_tenant_abc', + cancel_at_period_end: false, + current_period_start: 1700000000, + current_period_end: 1702592000, + items: {data: [{id: 'si_001'}]}, + }; + + stripeStub.subscriptions.retrieve.resolves(stubSub); + + const result = await service.getSubscription('sub_active_001'); + + expect(result.id).to.equal('sub_active_001'); + expect(result.status).to.equal('active'); + expect(result.customerId).to.equal('cus_tenant_abc'); + expect(result.currentPeriodStart).to.equal(1700000000); + expect(result.currentPeriodEnd).to.equal(1702592000); + expect(result.cancelAtPeriodEnd).to.be.false(); + }); + }); + + // ------------------------------------------------------------------------- + // updateSubscription + // ------------------------------------------------------------------------- + + describe('updateSubscription', () => { + it('updates an active subscription in place with proration', async () => { + const activeSub: StubStripeSubscription = { + id: 'sub_active_001', + status: 'active', + customer: 'cus_tenant_abc', + cancel_at_period_end: false, + current_period_start: 1700000000, + current_period_end: 1702592000, + items: {data: [{id: 'si_item_001'}]}, + }; + const updatedSub: StubStripeSubscription = {...activeSub}; + + stripeStub.subscriptions.retrieve.resolves(activeSub); + stripeStub.subscriptions.update.resolves(updatedSub); + + const updates: TSubscriptionUpdate = { + priceRefId: 'price_pro_999', + prorationBehavior: ProrationBehavior.CREATE_PRORATIONS, + }; + + const result = await service.updateSubscription( + 'sub_active_001', + updates, + ); + + expect(result.id).to.equal('sub_active_001'); + expect(result.status).to.equal('active'); + + // Verify stripe.subscriptions.cancel was NOT called (active path) + sinon.assert.notCalled(stripeStub.subscriptions.cancel); + sinon.assert.calledOnce(stripeStub.subscriptions.update); + + const updateArg = stripeStub.subscriptions.update.firstCall.args[1]; + expect(updateArg.proration_behavior).to.equal( + ProrationBehavior.CREATE_PRORATIONS, + ); + expect(updateArg.items[0].price).to.equal('price_pro_999'); + }); + + it('cancels an incomplete subscription and creates a replacement', async () => { + const incompleteSub: StubStripeSubscription = { + id: 'sub_incomplete_007', + status: 'incomplete', + customer: 'cus_tenant_abc', + cancel_at_period_end: false, + current_period_start: 0, + current_period_end: 0, + items: {data: [{id: 'si_item_007'}]}, + }; + + stripeStub.subscriptions.retrieve.resolves(incompleteSub); + stripeStub.subscriptions.cancel.resolves({}); + stripeStub.subscriptions.create.resolves({id: 'sub_replacement_008'}); + + const updates: TSubscriptionUpdate = {priceRefId: 'price_pro_999'}; + + const result = await service.updateSubscription( + 'sub_incomplete_007', + updates, + ); + + // a new subscription ID should be returned + expect(result.id).to.equal('sub_replacement_008'); + expect(result.status).to.equal('incomplete'); + + sinon.assert.calledOnce(stripeStub.subscriptions.cancel); + sinon.assert.calledOnce(stripeStub.subscriptions.create); + }); + }); + + // ------------------------------------------------------------------------- + // cancelSubscription + // ------------------------------------------------------------------------- + + describe('cancelSubscription', () => { + it('cancels the subscription and voids open invoices', async () => { + const openInvoice: StubStripeInvoice = { + id: 'in_open_001', + status: 'open', + }; + + stripeStub.subscriptions.cancel.resolves({}); + stripeStub.invoices.list.resolves({data: [openInvoice]}); + stripeStub.invoices.voidInvoice.resolves({}); + + await service.cancelSubscription('sub_active_001'); + + sinon.assert.calledOnce(stripeStub.subscriptions.cancel); + sinon.assert.calledOnceWithExactly( + stripeStub.invoices.voidInvoice, + 'in_open_001', + ); + sinon.assert.notCalled(stripeStub.invoices.finalizeInvoice); + }); + + it('finalizes then voids draft invoices on cancellation', async () => { + const draftInvoice: StubStripeInvoice = { + id: 'in_draft_002', + status: 'draft', + }; + + stripeStub.subscriptions.cancel.resolves({}); + stripeStub.invoices.list.resolves({data: [draftInvoice]}); + stripeStub.invoices.finalizeInvoice.resolves({}); + stripeStub.invoices.voidInvoice.resolves({}); + + await service.cancelSubscription('sub_active_001'); + + sinon.assert.calledOnceWithExactly( + stripeStub.invoices.finalizeInvoice, + 'in_draft_002', + ); + sinon.assert.calledOnceWithExactly( + stripeStub.invoices.voidInvoice, + 'in_draft_002', + ); + }); + + it('takes no invoice action for already-paid invoices', async () => { + const paidInvoice: StubStripeInvoice = { + id: 'in_paid_003', + status: 'paid', + }; + + stripeStub.subscriptions.cancel.resolves({}); + stripeStub.invoices.list.resolves({data: [paidInvoice]}); + + await service.cancelSubscription('sub_active_001'); + + sinon.assert.notCalled(stripeStub.invoices.voidInvoice); + sinon.assert.notCalled(stripeStub.invoices.finalizeInvoice); + }); + }); + + // ------------------------------------------------------------------------- + // pauseSubscription + // ------------------------------------------------------------------------- + + describe('pauseSubscription', () => { + it('pauses a subscription by setting mark_uncollectible behavior', async () => { + stripeStub.subscriptions.update.resolves({}); + + await service.pauseSubscription('sub_active_001'); + + sinon.assert.calledOnce(stripeStub.subscriptions.update); + const updateArg = stripeStub.subscriptions.update.firstCall.args[1]; + expect(updateArg.pause_collection?.behavior).to.equal( + 'mark_uncollectible', + ); + }); + }); + + // ------------------------------------------------------------------------- + // resumeSubscription + // ------------------------------------------------------------------------- + + describe('resumeSubscription', () => { + it('resumes a paused subscription by clearing pause_collection', async () => { + stripeStub.subscriptions.update.resolves({}); + + await service.resumeSubscription('sub_paused_001'); + + sinon.assert.calledOnce(stripeStub.subscriptions.update); + const callArg = stripeStub.subscriptions.update.firstCall.args[0]; + expect(callArg).to.equal('sub_paused_001'); + }); + }); + + // ------------------------------------------------------------------------- + // getInvoicePriceDetails + // ------------------------------------------------------------------------- + + describe('getInvoicePriceDetails', () => { + it('returns a correctly computed invoice price breakdown', async () => { + const fakeInvoice: StubStripeInvoiceDetail = { + currency: 'usd', + total: 5999, + total_tax_amounts: [{amount: 500}, {amount: 299}], + }; + + stripeStub.invoices.retrieve.resolves(fakeInvoice); + + const result = await service.getInvoicePriceDetails('in_123'); + + expect(result.currency).to.equal('USD'); + expect(result.totalAmount).to.equal(5999); + expect(result.taxAmount).to.equal(799); // 500 + 299 + expect(result.amountExcludingTax).to.equal(5200); // 5999 - 799 + }); + + it('returns zero tax when total_tax_amounts is empty', async () => { + stripeStub.invoices.retrieve.resolves({ + currency: 'eur', + total: 2000, + total_tax_amounts: [], + }); + + const result = await service.getInvoicePriceDetails('in_no_tax'); + + expect(result.taxAmount).to.equal(0); + expect(result.amountExcludingTax).to.equal(2000); + }); + }); + + // ------------------------------------------------------------------------- + // sendPaymentLink + // ------------------------------------------------------------------------- + + describe('sendPaymentLink', () => { + it('calls stripe.invoices.sendInvoice with the correct invoice ID', async () => { + // Stub retrieve to return a send_invoice, finalized (open) invoice + stripeStub.invoices.retrieve.resolves({ + id: 'in_link_001', + status: 'open', + collection_method: 'send_invoice', + }); + stripeStub.invoices.sendInvoice.resolves({}); + + await service.sendPaymentLink('in_link_001'); + + sinon.assert.calledOnceWithExactly( + stripeStub.invoices.sendInvoice, + 'in_link_001', + ); + }); + + it('finalizes a draft send_invoice before sending', async () => { + stripeStub.invoices.retrieve.resolves({ + id: 'in_draft_001', + status: 'draft', + collection_method: 'send_invoice', + }); + stripeStub.invoices.finalizeInvoice.resolves({}); + stripeStub.invoices.sendInvoice.resolves({}); + + await service.sendPaymentLink('in_draft_001'); + + sinon.assert.calledOnceWithExactly( + stripeStub.invoices.finalizeInvoice, + 'in_draft_001', + ); + sinon.assert.calledOnceWithExactly( + stripeStub.invoices.sendInvoice, + 'in_draft_001', + ); + }); + + it('skips sendInvoice for charge_automatically invoices', async () => { + stripeStub.invoices.retrieve.resolves({ + id: 'in_auto_001', + status: 'open', + collection_method: 'charge_automatically', + }); + + await service.sendPaymentLink('in_auto_001'); + + sinon.assert.notCalled(stripeStub.invoices.sendInvoice); + }); + + it('finalizes draft charge_automatically invoice without sending', async () => { + stripeStub.invoices.retrieve.resolves({ + id: 'in_auto_draft_001', + status: 'draft', + collection_method: 'charge_automatically', + }); + stripeStub.invoices.finalizeInvoice.resolves({}); + + await service.sendPaymentLink('in_auto_draft_001'); + + sinon.assert.calledOnceWithExactly( + stripeStub.invoices.finalizeInvoice, + 'in_auto_draft_001', + ); + sinon.assert.notCalled(stripeStub.invoices.sendInvoice); + }); + }); + + // ------------------------------------------------------------------------- + // checkProductExists + // ------------------------------------------------------------------------- + + describe('checkProductExists', () => { + it('returns true when the product exists and is active', async () => { + stripeStub.products.retrieve.resolves({active: true}); + + const result = await service.checkProductExists('prod_active_001'); + + expect(result).to.be.true(); + }); + + it('returns false when the product is archived (active: false)', async () => { + stripeStub.products.retrieve.resolves({active: false}); + + const result = await service.checkProductExists('prod_archived_002'); + + expect(result).to.be.false(); + }); + + it('returns false when Stripe signals resource_missing', async () => { + const notFoundError = Object.assign(new Error('No such product'), { + code: 'resource_missing', + }); + stripeStub.products.retrieve.rejects(notFoundError); + + const result = await service.checkProductExists('prod_gone_003'); + + expect(result).to.be.false(); + }); + + it('re-throws unexpected errors from Stripe', async () => { + const networkError = new Error('Network failure'); + stripeStub.products.retrieve.rejects(networkError); + + await expect( + service.checkProductExists('prod_error_004'), + ).to.be.rejectedWith('Network failure'); + }); + }); +}); diff --git a/src/keys.ts b/src/keys.ts index c1fd1cd..ee14b9d 100644 --- a/src/keys.ts +++ b/src/keys.ts @@ -1,6 +1,6 @@ import {BindingKey, CoreBindings} from '@loopback/core'; import {BillingComponent} from './component'; -import {IService} from './types'; +import {IService, ISubscriptionService} from './types'; /** * Binding keys used by this component. @@ -12,4 +12,13 @@ export namespace BillingComponentBindings { export const BillingProvider = BindingKey.create('sf.billing'); export const SDKProvider = BindingKey.create('sf.billing.sdk'); export const RestProvider = BindingKey.create('sf.billing.rest'); + /** + * Binding key for a provider that implements the full subscription lifecycle + * ({@link ISubscriptionService}). Bind your extended StripeService (or any + * other gateway implementation) here so controllers and services can inject + * subscription capabilities independently of one-time billing. + */ + export const SubscriptionProvider = BindingKey.create( + 'sf.billing.subscription', + ); } diff --git a/src/providers/billing.provider.ts b/src/providers/billing.provider.ts index 4ab4333..86f0518 100644 --- a/src/providers/billing.provider.ts +++ b/src/providers/billing.provider.ts @@ -99,29 +99,6 @@ export class BillingProvider implements Provider { } value() { - return { - createCustomer: async (customerDto: TCustomer) => - this.createCustomer(customerDto), - getCustomers: (customerId: string) => this.getCustomers(customerId), - updateCustomerById: (tenantId: string, customerDto: Partial) => - this.updateCustomerById(tenantId, customerDto), - deleteCustomer: (customerId: string) => this.deleteCustomer(customerId), - createPaymentSource: (paymentDto: TPaymentSource) => - this.createPaymentSource(paymentDto), - applyPaymentSourceForInvoice: ( - invoiceId: string, - transaction: Transaction, - ) => this.applyPaymentSourceForInvoice(invoiceId, transaction), - retrievePaymentSource: (paymentSourceId: string) => - this.retrievePaymentSource(paymentSourceId), - deletePaymentSource: (paymentSourceId: string) => - this.deletePaymentSource(paymentSourceId), - createInvoice: (invoice: TInvoice) => this.createInvoice(invoice), - retrieveInvoice: (invoiceId: string) => this.retrieveInvoice(invoiceId), - updateInvoice: (invoiceId: string, invoice: Partial) => - this.updateInvoice(invoiceId, invoice), - deleteInvoice: (invoiceId: string) => this.deleteInvoice(invoiceId), - getPaymentStatus: (invoiceId: string) => this.getPaymentStatus(invoiceId), - }; + return this.getProvider(); } } diff --git a/src/providers/sdk/stripe/adapter/index.ts b/src/providers/sdk/stripe/adapter/index.ts index 353cecf..ed2e797 100644 --- a/src/providers/sdk/stripe/adapter/index.ts +++ b/src/providers/sdk/stripe/adapter/index.ts @@ -1,3 +1,4 @@ export * from './customer.adapter'; export * from './invoice.adapter'; export * from './payment-source.adapter'; +export * from './subscription.adapter'; diff --git a/src/providers/sdk/stripe/adapter/subscription.adapter.ts b/src/providers/sdk/stripe/adapter/subscription.adapter.ts new file mode 100644 index 0000000..fb12150 --- /dev/null +++ b/src/providers/sdk/stripe/adapter/subscription.adapter.ts @@ -0,0 +1,67 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import Stripe from 'stripe'; +import { + IAdapter, + TSubscriptionCreate, + TSubscriptionResult, +} from '../../../../types'; + +/** + * Adapter that converts between the Stripe Subscription SDK object and the + * provider-agnostic {@link TSubscriptionResult} shape used throughout the + * library. + * + * Library consumers can subclass this adapter and re-bind it to customise the + * mapping — e.g. to expose additional Stripe-specific fields — without + * modifying {@link StripeService}. + * + * @example + * ```ts + * class MySubscriptionAdapter extends StripeSubscriptionAdapter { + * adaptToModel(resp: Stripe.Subscription): TSubscriptionResult { + * return { ...super.adaptToModel(resp), trialEnd: resp.trial_end }; + * } + * } + * ``` + */ +export class StripeSubscriptionAdapter + implements IAdapter +{ + /** + * Maps a raw Stripe Subscription object to the normalised + * {@link TSubscriptionResult}. + * + * @param resp - Raw Stripe Subscription returned by the SDK. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + adaptToModel(resp: any): TSubscriptionResult { + const sub = resp as Stripe.Subscription; + return { + id: sub.id, + status: sub.status, + customerId: + typeof sub.customer === 'string' ? sub.customer : sub.customer?.id, + currentPeriodStart: sub.current_period_start, + currentPeriodEnd: sub.current_period_end, + cancelAtPeriodEnd: sub.cancel_at_period_end, + }; + } + + /** + * Maps a {@link TSubscriptionCreate} to the Stripe subscription create + * parameters. + * + * @param data - Provider-agnostic subscription creation payload. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + adaptFromModel(data: Partial): any { + return { + customer: data.customerId, + items: data.priceRefId ? [{price: data.priceRefId}] : [], + collection_method: data.collectionMethod, + ...(data.daysUntilDue !== undefined && { + days_until_due: data.daysUntilDue, + }), + }; + } +} diff --git a/src/providers/sdk/stripe/stripe.service.ts b/src/providers/sdk/stripe/stripe.service.ts index 95b36a5..ec782c8 100644 --- a/src/providers/sdk/stripe/stripe.service.ts +++ b/src/providers/sdk/stripe/stripe.service.ts @@ -1,11 +1,23 @@ /* eslint-disable @typescript-eslint/naming-convention */ import {inject} from '@loopback/core'; import Stripe from 'stripe'; -import {TInvoice, Transaction} from '../../../types'; +import { + CollectionMethod, + RecurringInterval, + TInvoice, + TInvoicePrice, + TPrice, + TProduct, + TSubscriptionCreate, + TSubscriptionResult, + TSubscriptionUpdate, + Transaction, +} from '../../../types'; import { StripeCustomerAdapter, StripeInvoiceAdapter, StripePaymentAdapter, + StripeSubscriptionAdapter, } from './adapter'; import {StripeBindings} from './key'; import { @@ -16,10 +28,15 @@ import { StripeConfig, } from './type'; export class StripeService implements IStripeService { - private stripe: Stripe; + /** + * Stripe SDK instance. `protected` to allow subclasses (and test doubles) + * to substitute the instance without re-opening the class. + */ + protected stripe: Stripe; stripeCustomerAdapter: StripeCustomerAdapter; stripeInvoiceAdapter: StripeInvoiceAdapter; stripePaymentAdapter: StripePaymentAdapter; + stripeSubscriptionAdapter: StripeSubscriptionAdapter; constructor( @inject(StripeBindings.config, {optional: true}) @@ -31,6 +48,7 @@ export class StripeService implements IStripeService { this.stripeCustomerAdapter = new StripeCustomerAdapter(); this.stripeInvoiceAdapter = new StripeInvoiceAdapter(); this.stripePaymentAdapter = new StripePaymentAdapter(); + this.stripeSubscriptionAdapter = new StripeSubscriptionAdapter(); } async createCustomer(customerDto: IStripeCustomer): Promise { @@ -227,4 +245,262 @@ export class StripeService implements IStripeService { const invoice = await this.stripe.invoices.retrieve(invoiceId); return invoice.status === 'paid'; } + + // --------------------------------------------------------------------------- + // ISubscriptionService implementation + // --------------------------------------------------------------------------- + + /** + * Creates a new product in Stripe and returns the product's external ID. + * + * @param product - Product details (name, optional description and metadata). + * @returns The Stripe product ID. + */ + async createProduct(product: TProduct): Promise { + const created = await this.stripe.products.create({ + name: product.name, + description: product.description, + metadata: product.metadata, + }); + return created.id; + } + + /** + * Creates a recurring price in Stripe and returns the normalised {@link TPrice}. + * + * @param price - Price configuration including currency, amount and recurrence. + * @returns The created price with its Stripe-assigned ID. + */ + async createPrice(price: TPrice): Promise { + const created = await this.stripe.prices.create({ + currency: price.currency, + unit_amount: price.unitAmount, + product: price.product, + recurring: price.recurring + ? { + interval: price.recurring + .interval as Stripe.PriceCreateParams.Recurring.Interval, + interval_count: price.recurring.intervalCount, + } + : undefined, + metadata: price.metadata, + }); + + return { + id: created.id, + currency: created.currency, + unitAmount: created.unit_amount ?? 0, + product: + typeof created.product === 'string' + ? created.product + : (created.product?.id ?? ''), + recurring: created.recurring + ? { + interval: created.recurring.interval as RecurringInterval, + intervalCount: created.recurring.interval_count, + } + : undefined, + metadata: created.metadata as Record, + active: created.active, + }; + } + + /** + * Creates a new subscription in Stripe. + * + * Uses `payment_behavior: 'default_incomplete'` so the subscription starts + * in an `incomplete` state until the first payment is confirmed, which is + * the recommended Stripe pattern for SCA-compliant flows. + * + * @param subscription - Subscription parameters including customer, price and collection method. + * @returns The Stripe subscription ID. + */ + async createSubscription(subscription: TSubscriptionCreate): Promise { + const created = await this.stripe.subscriptions.create({ + customer: subscription.customerId, + items: [{price: subscription.priceRefId}], + collection_method: subscription.collectionMethod, + days_until_due: subscription.daysUntilDue, + payment_behavior: (this.stripeConfig.defaultPaymentBehavior ?? + 'default_incomplete') as Stripe.SubscriptionCreateParams.PaymentBehavior, + }); + return created.id; + } + + /** + * Retrieves the current state of a subscription from Stripe. + * + * @param subscriptionId - The Stripe subscription ID. + * @returns A normalised {@link TSubscriptionResult}. + */ + async getSubscription(subscriptionId: string): Promise { + const subscription = + await this.stripe.subscriptions.retrieve(subscriptionId); + return this.stripeSubscriptionAdapter.adaptToModel(subscription); + } + + /** + * Upgrades or downgrades an existing subscription. + * + * Handles the edge case where a subscription is still `incomplete` (first + * payment not yet confirmed): the incomplete subscription is cancelled and a + * fresh one is created so the customer can retry payment. + * + * For active subscriptions the Stripe proration behaviour is controlled by + * {@link TSubscriptionUpdate.prorationBehavior}. + * + * @param subscriptionId - The Stripe subscription ID to modify. + * @param updates - The new price and optional proration behaviour. + * @returns A normalised {@link TSubscriptionResult} reflecting the change. + */ + async updateSubscription( + subscriptionId: string, + updates: TSubscriptionUpdate, + ): Promise { + const existing = await this.stripe.subscriptions.retrieve(subscriptionId); + + if (existing.status === 'incomplete') { + // Cancel the incomplete subscription and create a fresh one so the + // customer gets a new payment confirmation link. + await this.stripe.subscriptions.cancel(subscriptionId); + const newId = await this.createSubscription({ + customerId: existing.customer as string, + priceRefId: updates.priceRefId ?? '', + collectionMethod: CollectionMethod.CHARGE_AUTOMATICALLY, + }); + return { + id: newId, + status: 'incomplete', + customerId: existing.customer as string, + }; + } + + const priceItemId = existing.items.data[0].id; + const updated = await this.stripe.subscriptions.update(subscriptionId, { + proration_behavior: + updates.prorationBehavior as Stripe.SubscriptionUpdateParams.ProrationBehavior, + items: [{id: priceItemId, price: updates.priceRefId}], + }); + return this.stripeSubscriptionAdapter.adaptToModel(updated); + } + + /** + * Cancels a subscription immediately with proration. + * + * After cancellation any open invoices are voided and any draft invoices are + * finalised then voided, ensuring the customer is not charged for the + * remaining period. + * + * @param subscriptionId - The Stripe subscription ID to cancel. + */ + async cancelSubscription(subscriptionId: string): Promise { + await this.stripe.subscriptions.cancel(subscriptionId); + + // Best-effort: void any remaining open/draft invoices after cancellation. + // Errors here should not fail the cancel response. + try { + const invoices = await this.stripe.invoices.list({ + subscription: subscriptionId, + }); + + await Promise.all( + invoices.data.map(async invoice => { + if (invoice.status === 'open' && invoice.id) { + return this.stripe.invoices.voidInvoice(invoice.id); + } else if (invoice.status === 'draft' && invoice.id) { + await this.stripe.invoices.finalizeInvoice(invoice.id); + return this.stripe.invoices.voidInvoice(invoice.id); + } else { + return Promise.resolve(); + } + }), + ); + } catch (_err) { + // Non-fatal — subscription is already cancelled in Stripe + } + } + + /** + * Pauses a subscription by marking future invoices as uncollectible. + * The subscription remains active in Stripe but no charges are attempted. + * + * @param subscriptionId - The Stripe subscription ID to pause. + */ + async pauseSubscription(subscriptionId: string): Promise { + await this.stripe.subscriptions.update(subscriptionId, { + pause_collection: {behavior: 'mark_uncollectible'}, + }); + } + + /** + * Resumes a previously paused subscription by clearing the pause collection. + * + * @param subscriptionId - The Stripe subscription ID to resume. + */ + async resumeSubscription(subscriptionId: string): Promise { + await this.stripe.subscriptions.update(subscriptionId, { + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + pause_collection: '' as any, // NOSONAR – Stripe uses empty string to clear pause_collection + }); + } + + /** + * Returns a detailed price breakdown for an invoice including tax and the + * amount excluding tax. + * + * @param invoiceId - The Stripe invoice ID. + * @returns {@link TInvoicePrice} with amounts in the invoice's minor currency unit. + */ + async getInvoicePriceDetails(invoiceId: string): Promise { + const invoice = await this.stripe.invoices.retrieve(invoiceId); + const taxAmount = + invoice.total_tax_amounts?.reduce((sum, tax) => sum + tax.amount, 0) ?? 0; + + return { + currency: invoice.currency.toUpperCase(), + totalAmount: invoice.total, + taxAmount, + amountExcludingTax: invoice.total - taxAmount, + }; + } + + /** + * Sends a hosted payment link for the given invoice to the customer's email. + * + * @param invoiceId - The Stripe invoice ID. + */ + async sendPaymentLink(invoiceId: string): Promise { + const invoice = await this.stripe.invoices.retrieve(invoiceId); + // sendInvoice is only valid for 'send_invoice' collection method. + // For 'charge_automatically' invoices, Stripe handles collection automatically; + // finalize the invoice if it is still a draft so it becomes collectable. + if (invoice.collection_method !== 'send_invoice') { + if (invoice.status === 'draft') { + await this.stripe.invoices.finalizeInvoice(invoiceId); + } + return; + } + if (invoice.status === 'draft') { + await this.stripe.invoices.finalizeInvoice(invoiceId); + } + await this.stripe.invoices.sendInvoice(invoiceId); + } + + /** + * Checks whether a product exists in Stripe and is currently active. + * + * @param productId - The Stripe product ID. + * @returns `true` if the product is active, `false` if it is archived or not found. + */ + async checkProductExists(productId: string): Promise { + try { + const product = await this.stripe.products.retrieve(productId); + return product.active === true; + } catch (error) { + if ((error as {code?: string}).code === 'resource_missing') { + return false; + } + throw error; + } + } } diff --git a/src/providers/sdk/stripe/type/index.ts b/src/providers/sdk/stripe/type/index.ts index e5fa47b..112af8a 100644 --- a/src/providers/sdk/stripe/type/index.ts +++ b/src/providers/sdk/stripe/type/index.ts @@ -1,11 +1,16 @@ -import {IService} from '../../../../types'; +import {IService, ISubscriptionService} from '../../../../types'; -export interface StripeConfig { - secretKey: string; -} - -export interface IStripeService extends IService {} +/** + * Full Stripe service interface combining one-time billing ({@link IService}) + * and recurring-subscription management ({@link ISubscriptionService}). + * + * Implementors can bind to {@link BillingComponentBindings.SDKProvider} for + * one-time billing OR to {@link BillingComponentBindings.SubscriptionProvider} + * for subscription operations, depending on their needs (ISP). + */ +export interface IStripeService extends IService, ISubscriptionService {} export * from './customer.type'; export * from './invoice.type'; export * from './payment-source.type'; +export * from './stripe-config.type'; diff --git a/src/providers/sdk/stripe/type/stripe-config.type.ts b/src/providers/sdk/stripe/type/stripe-config.type.ts new file mode 100644 index 0000000..3685c4e --- /dev/null +++ b/src/providers/sdk/stripe/type/stripe-config.type.ts @@ -0,0 +1,14 @@ +export interface StripeConfig { + secretKey: string; + /** + * Controls how Stripe handles payment during subscription creation. + * Defaults to `'default_incomplete'` (SCA-compliant: subscription starts + * incomplete until the first payment is confirmed). + * + * Set to `'allow_incomplete'` or `'error_if_incomplete'` to change the + * behaviour for your integration. + * + * @see https://stripe.com/docs/api/subscriptions/create#create_subscription-payment_behavior + */ + defaultPaymentBehavior?: string; +} diff --git a/src/types.ts b/src/types.ts index cf23d35..71b77f2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -119,6 +119,174 @@ export type InvoiceStatus = | 'voided' | 'pending'; +// --------------------------------------------------------------------------- +// Subscription Management Types +// --------------------------------------------------------------------------- + +/** + * Supported billing collection methods for recurring subscriptions. + */ +export enum CollectionMethod { + CHARGE_AUTOMATICALLY = 'charge_automatically', + SEND_INVOICE = 'send_invoice', +} + +/** + * Supported recurring billing intervals. + */ +export enum RecurringInterval { + DAY = 'day', + WEEK = 'week', + MONTH = 'month', + YEAR = 'year', +} + +/** + * Controls how prorations are calculated when a subscription is updated. + */ +export enum ProrationBehavior { + CREATE_PRORATIONS = 'create_prorations', + NONE = 'none', + ALWAYS_INVOICE = 'always_invoice', +} + +/** + * Parameters required to create a product in the billing provider. + */ +export interface TProduct { + name: string; + description?: string; + metadata?: Record; +} + +/** + * Provider-agnostic representation of a recurring price / plan. + */ +export interface TPrice { + id?: string; + currency: string; + unitAmount: number; + /** External product ID that this price belongs to. */ + product: string; + recurring?: { + interval: RecurringInterval; + intervalCount: number; + }; + metadata?: Record; + active?: boolean; +} + +/** + * Parameters required to create a new subscription. + */ +export interface TSubscriptionCreate { + customerId: string; + /** Price / plan reference ID from the billing provider. */ + priceRefId: string; + collectionMethod: CollectionMethod; + /** Number of days after which the invoice is due (applicable for send_invoice). */ + daysUntilDue?: number; +} + +/** + * Parameters allowed when upgrading or downgrading an existing subscription. + */ +export interface TSubscriptionUpdate { + /** New price / plan reference ID. */ + priceRefId?: string; + prorationBehavior?: ProrationBehavior; +} + +/** + * Provider-agnostic subscription result returned after create / update / get. + */ +export interface TSubscriptionResult { + id: string; + status: string; + customerId: string; + currentPeriodStart?: number; + currentPeriodEnd?: number; + cancelAtPeriodEnd?: boolean; +} + +/** + * Detailed price breakdown of an invoice. + */ +export interface TInvoicePrice { + currency: string; + totalAmount: number; + taxAmount: number; + amountExcludingTax: number; +} + +/** + * Interface that any billing provider must implement to support the full + * recurring-subscription lifecycle. + * + * Keeps subscription concerns separated from one-time billing (IService), + * following the Interface Segregation Principle. + */ +export interface ISubscriptionService { + /** + * Creates a product in the billing provider and returns its external ID. + */ + createProduct(product: TProduct): Promise; + + /** + * Creates a price (recurring billing configuration) and returns the full price object. + */ + createPrice(price: TPrice): Promise; + + /** + * Creates a new recurring subscription and returns its external ID. + */ + createSubscription(subscription: TSubscriptionCreate): Promise; + + /** + * Retrieves the current state of a subscription by its external ID. + */ + getSubscription(subscriptionId: string): Promise; + + /** + * Upgrades or downgrades an active subscription. + * Handles the incomplete-subscription edge case automatically. + */ + updateSubscription( + subscriptionId: string, + updates: TSubscriptionUpdate, + ): Promise; + + /** + * Cancels a subscription immediately with proration and voids open invoices. + */ + cancelSubscription(subscriptionId: string): Promise; + + /** + * Pauses a subscription (marks outstanding invoices as uncollectible). + */ + pauseSubscription(subscriptionId: string): Promise; + + /** + * Resumes a previously paused subscription. + */ + resumeSubscription(subscriptionId: string): Promise; + + /** + * Returns a detailed price breakdown (total, tax, amount excluding tax) for an invoice. + */ + getInvoicePriceDetails(invoiceId: string): Promise; + + /** + * Sends the hosted payment link for a given invoice to the customer. + */ + sendPaymentLink(invoiceId: string): Promise; + + /** + * Checks whether a product exists and is still active in the billing provider. + */ + checkProductExists(productId: string): Promise; +} + export const enum ServiceType { SDK, REST, From ecde8274c2d1255f4bdc6d77c0463810cecc0293 Mon Sep 17 00:00:00 2001 From: Piyush Singh Gaur Date: Wed, 25 Mar 2026 10:03:30 +0530 Subject: [PATCH 2/7] fix(provider): fix sonar issues --- src/providers/sdk/stripe/stripe.service.ts | 57 +++++++++++----------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/src/providers/sdk/stripe/stripe.service.ts b/src/providers/sdk/stripe/stripe.service.ts index ec782c8..1bf2ce2 100644 --- a/src/providers/sdk/stripe/stripe.service.ts +++ b/src/providers/sdk/stripe/stripe.service.ts @@ -204,39 +204,37 @@ export class StripeService implements IStripeService { invoiceId: string, invoice: Partial, ): Promise { - // Create the update object conditionally based on which fields are defined const updateData: Stripe.InvoiceUpdateParams = {}; - if (invoice.shippingAddress) { - updateData.shipping_details = { - name: [ - invoice.shippingAddress.firstName ?? '', // Avoid 'undefined' in the name - invoice.shippingAddress.lastName ?? '', - ] - .join(' ') - .trim(), // Trim to avoid extra spaces - address: { - line1: invoice.shippingAddress.line1 ?? undefined, // Only set if defined - line2: invoice.shippingAddress.line2 ?? undefined, - city: invoice.shippingAddress.city ?? undefined, - state: invoice.shippingAddress.state ?? undefined, - postal_code: invoice.shippingAddress.zip ?? undefined, - country: invoice.shippingAddress.country ?? undefined, - }, - phone: invoice.shippingAddress.phone ?? undefined, // Only set phone if provided - }; + updateData.shipping_details = this.buildShippingDetails( + invoice.shippingAddress, + ); } - - // Call the Stripe API with the built update data const updatedInvoice = await this.stripe.invoices.update( invoiceId, updateData, ); - - // Adapt the updated invoice to your model return this.stripeInvoiceAdapter.adaptToModel(updatedInvoice); } + private buildShippingDetails( + addr: IStripeInvoice['shippingAddress'], + ): Stripe.InvoiceUpdateParams.ShippingDetails { + const name = [addr?.firstName ?? '', addr?.lastName ?? ''].join(' ').trim(); + return { + name, + address: { + line1: addr?.line1, + line2: addr?.line2, + city: addr?.city, + state: addr?.state, + postal_code: addr?.zip, + country: addr?.country, + }, + phone: addr?.phone, + }; + } + async deleteInvoice(invoiceId: string): Promise { await this.stripe.invoices.del(invoiceId); } @@ -407,16 +405,19 @@ export class StripeService implements IStripeService { invoices.data.map(async invoice => { if (invoice.status === 'open' && invoice.id) { return this.stripe.invoices.voidInvoice(invoice.id); - } else if (invoice.status === 'draft' && invoice.id) { + } + if (invoice.status === 'draft' && invoice.id) { await this.stripe.invoices.finalizeInvoice(invoice.id); return this.stripe.invoices.voidInvoice(invoice.id); - } else { - return Promise.resolve(); } }), ); - } catch (_err) { - // Non-fatal — subscription is already cancelled in Stripe + } catch (err) { + // Non-fatal — subscription is already cancelled in Stripe; log for observability + console.info( + '[StripeService] cancelSubscription: invoice cleanup failed', + err, + ); } } From 2d8499c2b1cbd0ba4b9d038a3463ffc11a264027 Mon Sep 17 00:00:00 2001 From: Piyush Singh Gaur Date: Wed, 25 Mar 2026 17:41:14 +0530 Subject: [PATCH 3/7] feat(provider): implement a comprehensive subscription lifecycle in stripe Implement a comprehensive subscription lifecycle within loopback4-billing to support automatedrecurring billing, including subscription creation, upgrades and downgrades, renewals,cancellations, and proration, ensuring consistency and scalability for SaaS monetization GH-0 --- .../chargebee-subscription.service.unit.ts | 499 ++++++++++++++++++ src/providers/sdk/chargebee/adapter/index.ts | 1 + .../chargebee/adapter/subscription.adapter.ts | 75 +++ .../sdk/chargebee/charge-bee.service.ts | 353 ++++++++++++- .../chargebee/type/chargebee-config.type.ts | 38 ++ src/providers/sdk/chargebee/type/index.ts | 26 +- 6 files changed, 977 insertions(+), 15 deletions(-) create mode 100644 src/__tests__/unit/chargebee-subscription.service.unit.ts create mode 100644 src/providers/sdk/chargebee/adapter/subscription.adapter.ts create mode 100644 src/providers/sdk/chargebee/type/chargebee-config.type.ts diff --git a/src/__tests__/unit/chargebee-subscription.service.unit.ts b/src/__tests__/unit/chargebee-subscription.service.unit.ts new file mode 100644 index 0000000..baaf73e --- /dev/null +++ b/src/__tests__/unit/chargebee-subscription.service.unit.ts @@ -0,0 +1,499 @@ +import {expect, sinon} from '@loopback/testlab'; +import chargebee from 'chargebee'; +import {ChargeBeeService} from '../../providers/sdk/chargebee/charge-bee.service'; +import { + CollectionMethod, + ProrationBehavior, + RecurringInterval, + TPrice, + TProduct, + TSubscriptionCreate, + TSubscriptionUpdate, +} from '../../types'; + +// --------------------------------------------------------------------------- +// Helper — builds a fake Chargebee subscription object +// --------------------------------------------------------------------------- + +function makeSubscription(overrides: object = {}) { + return { + id: 'sub_cb_001', + status: 'active', + customer_id: 'cust_tenant_abc', + current_term_start: 1700000000, + current_term_end: 1702592000, + cancel_at_period_end: false, + ...overrides, + }; +} + +// --------------------------------------------------------------------------- +// Suite +// --------------------------------------------------------------------------- + +describe('ChargeBeeService - Subscription Management', () => { + let service: ChargeBeeService; + let sandbox: sinon.SinonSandbox; + + /** + * Stub every chargebee API call so no network requests are made. + * The chargebee SDK uses a builder pattern: chargebee.resource.action(params).request() + * so each stub must return an object with a `.request` stub. + * Cast through `unknown` to satisfy the strict ChargebeeRequest generic type. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + function stubCb(returnValue: object): any { + // NOSONAR + return { + request: sinon.stub().resolves(returnValue), + setIdempotencyKey: sinon.stub().returnsThis(), + headers: sinon.stub().returnsThis(), + }; + } + + beforeEach(() => { + sandbox = sinon.createSandbox(); + + // Instantiate with dummy config — chargebee.configure is stubbed below + sandbox.stub(chargebee, 'configure'); + service = new ChargeBeeService({site: 'test-site', apiKey: 'test-key'}); + }); + + afterEach(() => { + sandbox.restore(); + }); + + // ------------------------------------------------------------------------- + // createProduct + // ------------------------------------------------------------------------- + + describe('createProduct', () => { + it('creates a plan-type Item and returns its ID', async () => { + const itemStub = sandbox + .stub(chargebee.item, 'create') + .returns(stubCb({item: {id: 'enterprise-plan'}})); + + const product: TProduct = { + name: 'Enterprise Plan', + description: 'Full-featured tier', + metadata: {tier: 'enterprise'}, + }; + + const result = await service.createProduct(product); + + expect(result).to.equal('enterprise-plan'); + sinon.assert.calledOnce(itemStub); + + const callArg = itemStub.firstCall.args[0]; + // ID is derived from the name + expect(callArg.id).to.equal('enterprise-plan'); + expect(callArg.type).to.equal('plan'); + }); + + it('generates a URL-safe ID from the product name', async () => { + const itemStub = sandbox + .stub(chargebee.item, 'create') + .returns(stubCb({item: {id: 'my-saas-product'}})); + + await service.createProduct({name: 'My SaaS Product!'}); + + const callArg = itemStub.firstCall.args[0]; + expect(callArg.id).to.equal('my-saas-product'); + }); + + it('uses defaultItemFamilyId from config when provided', async () => { + const customService = new ChargeBeeService({ + site: 'test-site', + apiKey: 'test-key', + defaultItemFamilyId: 'saas-plans', + }); + const itemStub = sandbox + .stub(chargebee.item, 'create') + .returns(stubCb({item: {id: 'enterprise-plan'}})); + + await customService.createProduct({name: 'Enterprise Plan'}); + + const callArg = itemStub.firstCall.args[0]; + expect(callArg.item_family_id).to.equal('saas-plans'); + }); + + it('falls back to "default" item_family_id when not configured', async () => { + const itemStub = sandbox + .stub(chargebee.item, 'create') + .returns(stubCb({item: {id: 'enterprise-plan'}})); + + await service.createProduct({name: 'Enterprise Plan'}); + + const callArg = itemStub.firstCall.args[0]; + expect(callArg.item_family_id).to.equal('default'); + }); + }); + + // ------------------------------------------------------------------------- + // createPrice + // ------------------------------------------------------------------------- + + describe('createPrice', () => { + it('creates an ItemPrice and maps the response to TPrice', async () => { + const itemPriceResponse = { + item_price: { + id: 'enterprise-plan-usd-monthly', + item_id: 'enterprise-plan', + currency_code: 'USD', + price: 4999, + period_unit: 'month', + period: 1, + status: 'active', + }, + }; + + sandbox + .stub(chargebee.item_price, 'create') + .returns(stubCb(itemPriceResponse)); + + const priceInput: TPrice = { + currency: 'usd', + unitAmount: 4999, + product: 'enterprise-plan', + recurring: {interval: RecurringInterval.MONTH, intervalCount: 1}, + }; + + const result = await service.createPrice(priceInput); + + expect(result.id).to.equal('enterprise-plan-usd-monthly'); + expect(result.currency).to.equal('usd'); + expect(result.unitAmount).to.equal(4999); + expect(result.product).to.equal('enterprise-plan'); + expect(result.recurring?.interval).to.equal(RecurringInterval.MONTH); + expect(result.recurring?.intervalCount).to.equal(1); + expect(result.active).to.be.true(); + }); + + it('returns undefined recurring when no period_unit is set', async () => { + sandbox.stub(chargebee.item_price, 'create').returns( + stubCb({ + item_price: { + id: 'setup-fee', + item_id: 'setup', + currency_code: 'USD', + price: 999, + period_unit: null, + period: null, + status: 'active', + }, + }), + ); + + const result = await service.createPrice({ + currency: 'usd', + unitAmount: 999, + product: 'setup', + }); + + expect(result.recurring).to.be.undefined(); + }); + }); + + // ------------------------------------------------------------------------- + // createSubscription + // ------------------------------------------------------------------------- + + describe('createSubscription', () => { + it('creates a subscription and returns its Chargebee ID', async () => { + const createStub = sandbox + .stub(chargebee.subscription, 'create_with_items') + .returns(stubCb({subscription: makeSubscription()})); + + const subscriptionInput: TSubscriptionCreate = { + customerId: 'cust_tenant_abc', + priceRefId: 'enterprise-plan-usd-monthly', + collectionMethod: CollectionMethod.CHARGE_AUTOMATICALLY, + }; + + const result = await service.createSubscription(subscriptionInput); + + expect(result).to.equal('sub_cb_001'); + sinon.assert.calledOnce(createStub); + + const [customerId, params] = createStub.firstCall.args; + expect(customerId).to.equal('cust_tenant_abc'); + expect(params.subscription_items[0].item_price_id).to.equal( + 'enterprise-plan-usd-monthly', + ); + expect(params.auto_collection).to.equal('on'); + }); + + it('sets auto_collection off and net_term_days for send_invoice', async () => { + const createStub = sandbox + .stub(chargebee.subscription, 'create_with_items') + .returns(stubCb({subscription: makeSubscription()})); + + await service.createSubscription({ + customerId: 'cust_tenant_abc', + priceRefId: 'enterprise-plan-usd-monthly', + collectionMethod: CollectionMethod.SEND_INVOICE, + daysUntilDue: 14, + }); + + const [, params] = createStub.firstCall.args; + expect(params.auto_collection).to.equal('off'); + expect(params.net_term_days).to.equal(14); + }); + }); + + // ------------------------------------------------------------------------- + // getSubscription + // ------------------------------------------------------------------------- + + describe('getSubscription', () => { + it('retrieves and maps a subscription to TSubscriptionResult', async () => { + sandbox + .stub(chargebee.subscription, 'retrieve') + .returns(stubCb({subscription: makeSubscription()})); + + const result = await service.getSubscription('sub_cb_001'); + + expect(result.id).to.equal('sub_cb_001'); + expect(result.status).to.equal('active'); + expect(result.customerId).to.equal('cust_tenant_abc'); + expect(result.currentPeriodStart).to.equal(1700000000); + expect(result.currentPeriodEnd).to.equal(1702592000); + expect(result.cancelAtPeriodEnd).to.be.false(); + }); + }); + + // ------------------------------------------------------------------------- + // updateSubscription + // ------------------------------------------------------------------------- + + describe('updateSubscription', () => { + it('upgrades a subscription to a new item price', async () => { + const updateStub = sandbox + .stub(chargebee.subscription, 'update_for_items') + .returns(stubCb({subscription: makeSubscription()})); + + const updates: TSubscriptionUpdate = { + priceRefId: 'pro-plan-usd-monthly', + prorationBehavior: ProrationBehavior.CREATE_PRORATIONS, + }; + + const result = await service.updateSubscription('sub_cb_001', updates); + + expect(result.id).to.equal('sub_cb_001'); + sinon.assert.calledOnce(updateStub); + + const [subId, params] = updateStub.firstCall.args; + expect(subId).to.equal('sub_cb_001'); + expect(params.subscription_items[0].item_price_id).to.equal( + 'pro-plan-usd-monthly', + ); + }); + + it('disables proration when prorationBehavior is none', async () => { + const updateStub = sandbox + .stub(chargebee.subscription, 'update_for_items') + .returns(stubCb({subscription: makeSubscription()})); + + await service.updateSubscription('sub_cb_001', { + priceRefId: 'basic-plan-usd', + prorationBehavior: ProrationBehavior.NONE, + }); + + const [, params] = updateStub.firstCall.args; + // ProrationBehavior.NONE -> prorate: false + expect(params!.prorate).to.be.false(); + }); + }); + + // ------------------------------------------------------------------------- + // cancelSubscription + // ------------------------------------------------------------------------- + + describe('cancelSubscription', () => { + it('cancels a subscription immediately with customer_request reason', async () => { + const cancelStub = sandbox + .stub(chargebee.subscription, 'cancel_for_items') + .returns( + stubCb({subscription: makeSubscription({status: 'cancelled'})}), + ); + + await service.cancelSubscription('sub_cb_001'); + + sinon.assert.calledOnce(cancelStub); + const [subId, params] = cancelStub.firstCall.args as [ + string, + Record, + ]; + expect(subId).to.equal('sub_cb_001'); + expect(params.end_of_term).to.be.false(); + expect(params.cancel_reason_code).to.equal('customer_request'); + }); + + it('uses cancelAtEndOfTerm and defaultCancelReasonCode from config when provided', async () => { + const customService = new ChargeBeeService({ + site: 'test-site', + apiKey: 'test-key', + cancelAtEndOfTerm: true, + defaultCancelReasonCode: 'not_paid', + }); + const cancelStub = sandbox + .stub(chargebee.subscription, 'cancel_for_items') + .returns( + stubCb({subscription: makeSubscription({status: 'cancelled'})}), + ); + + await customService.cancelSubscription('sub_cb_config'); + + const [, params] = cancelStub.firstCall.args as [ + string, + Record, + ]; + expect(params.end_of_term).to.be.true(); + expect(params.cancel_reason_code).to.equal('not_paid'); + }); + }); + + // ------------------------------------------------------------------------- + // pauseSubscription + // ------------------------------------------------------------------------- + + describe('pauseSubscription', () => { + it('pauses a subscription', async () => { + const pauseStub = sandbox + .stub(chargebee.subscription, 'pause') + .returns(stubCb({subscription: makeSubscription({status: 'paused'})})); + + await service.pauseSubscription('sub_cb_001'); + + sinon.assert.calledOnceWithExactly(pauseStub, 'sub_cb_001', {}); + }); + }); + + // ------------------------------------------------------------------------- + // resumeSubscription + // ------------------------------------------------------------------------- + + describe('resumeSubscription', () => { + it('resumes a paused subscription', async () => { + const resumeStub = sandbox + .stub(chargebee.subscription, 'resume') + .returns(stubCb({subscription: makeSubscription()})); + + await service.resumeSubscription('sub_cb_001'); + + sinon.assert.calledOnceWithExactly(resumeStub, 'sub_cb_001', {}); + }); + }); + + // ------------------------------------------------------------------------- + // getInvoicePriceDetails + // ------------------------------------------------------------------------- + + describe('getInvoicePriceDetails', () => { + it('returns correctly computed price breakdown', async () => { + sandbox.stub(chargebee.invoice, 'retrieve').returns( + stubCb({ + invoice: { + currency_code: 'usd', + total: 5999, + tax: 499, + }, + }), + ); + + const result = await service.getInvoicePriceDetails('inv_cb_001'); + + expect(result.currency).to.equal('USD'); + expect(result.totalAmount).to.equal(5999); + expect(result.taxAmount).to.equal(499); + expect(result.amountExcludingTax).to.equal(5500); + }); + + it('handles zero tax gracefully', async () => { + sandbox.stub(chargebee.invoice, 'retrieve').returns( + stubCb({ + invoice: {currency_code: 'eur', total: 2000, tax: 0}, + }), + ); + + const result = await service.getInvoicePriceDetails('inv_cb_zero_tax'); + + expect(result.taxAmount).to.equal(0); + expect(result.amountExcludingTax).to.equal(2000); + }); + }); + + // ------------------------------------------------------------------------- + // sendPaymentLink + // ------------------------------------------------------------------------- + + describe('sendPaymentLink', () => { + it('calls chargebee.invoice.collect_payment to trigger payment link delivery', async () => { + const collectStub = sandbox + .stub(chargebee.invoice, 'collect_payment') + .returns(stubCb({invoice: {id: 'inv_cb_001'}})); + + await service.sendPaymentLink('inv_cb_001'); + + sinon.assert.calledOnce(collectStub); + const [invoiceId] = collectStub.firstCall.args; + expect(invoiceId).to.equal('inv_cb_001'); + }); + }); + + // ------------------------------------------------------------------------- + // checkProductExists + // ------------------------------------------------------------------------- + + describe('checkProductExists', () => { + it('returns true when the item is active', async () => { + sandbox + .stub(chargebee.item, 'retrieve') + .returns(stubCb({item: {id: 'enterprise-plan', status: 'active'}})); + + const result = await service.checkProductExists('enterprise-plan'); + + expect(result).to.be.true(); + }); + + it('returns false when the item is archived', async () => { + sandbox + .stub(chargebee.item, 'retrieve') + .returns(stubCb({item: {id: 'old-plan', status: 'archived'}})); + + const result = await service.checkProductExists('old-plan'); + + expect(result).to.be.false(); + }); + + it('returns false when Chargebee signals resource_not_found', async () => { + sandbox + .stub(chargebee.item, 'retrieve') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .returns({ + request: sinon.stub().rejects({api_error_code: 'resource_not_found'}), + setIdempotencyKey: sinon.stub().returnsThis(), + headers: sinon.stub().returnsThis(), + } as any); // NOSONAR + + const result = await service.checkProductExists('missing-plan'); + + expect(result).to.be.false(); + }); + + it('re-throws unexpected errors from Chargebee', async () => { + sandbox + .stub(chargebee.item, 'retrieve') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .returns({ + request: sinon.stub().rejects(new Error('Network timeout')), + setIdempotencyKey: sinon.stub().returnsThis(), + headers: sinon.stub().returnsThis(), + } as any); // NOSONAR + + await expect(service.checkProductExists('flaky-plan')).to.be.rejectedWith( + Error, + ); + }); + }); +}); diff --git a/src/providers/sdk/chargebee/adapter/index.ts b/src/providers/sdk/chargebee/adapter/index.ts index 353cecf..ed2e797 100644 --- a/src/providers/sdk/chargebee/adapter/index.ts +++ b/src/providers/sdk/chargebee/adapter/index.ts @@ -1,3 +1,4 @@ export * from './customer.adapter'; export * from './invoice.adapter'; export * from './payment-source.adapter'; +export * from './subscription.adapter'; diff --git a/src/providers/sdk/chargebee/adapter/subscription.adapter.ts b/src/providers/sdk/chargebee/adapter/subscription.adapter.ts new file mode 100644 index 0000000..8b87634 --- /dev/null +++ b/src/providers/sdk/chargebee/adapter/subscription.adapter.ts @@ -0,0 +1,75 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { + IAdapter, + TSubscriptionCreate, + TSubscriptionResult, +} from '../../../../types'; + +/** + * Adapter that converts between the raw Chargebee Subscription object and the + * provider-agnostic {@link TSubscriptionResult} shape used throughout the + * library. + * + * Library consumers can subclass this adapter and re-assign + * `service.chargebeeSubscriptionAdapter` to customise the mapping without + * modifying {@link ChargeBeeService}. + * + * @example + * ```ts + * class MyAdapter extends ChargebeeSubscriptionAdapter { + * adaptToModel(resp: unknown): TSubscriptionResult { + * const base = super.adaptToModel(resp); + * const raw = resp as {trial_end?: number}; + * return {...base, trialEnd: raw.trial_end}; + * } + * } + * // then: + * service.chargebeeSubscriptionAdapter = new MyAdapter(); + * ``` + */ +export class ChargebeeSubscriptionAdapter + implements IAdapter +{ + /** + * Maps a raw Chargebee Subscription object to the normalised + * {@link TSubscriptionResult}. + * + * @param resp - Raw Chargebee Subscription returned by the SDK. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + adaptToModel(resp: any): TSubscriptionResult { + // NOSONAR + return { + id: resp.id, + status: resp.status, + customerId: resp.customer_id, + currentPeriodStart: resp.current_term_start, + currentPeriodEnd: resp.current_term_end, + cancelAtPeriodEnd: resp.cancel_at_period_end ?? false, + }; + } + + /** + * Maps a {@link TSubscriptionCreate} to Chargebee `create_with_items` + * parameters. + * + * @param data - Provider-agnostic subscription creation payload. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + adaptFromModel(data: Partial): any { + return { + subscription_items: data.priceRefId + ? [{item_price_id: data.priceRefId}] + : [], + discounts: [], + ...(data.collectionMethod === 'send_invoice' + ? { + auto_collection: 'off' as const, + ...(data.daysUntilDue !== undefined && { + net_term_days: data.daysUntilDue, + }), + } + : {auto_collection: 'on' as const}), + }; + } +} diff --git a/src/providers/sdk/chargebee/charge-bee.service.ts b/src/providers/sdk/chargebee/charge-bee.service.ts index f98e19c..9f4fbb2 100644 --- a/src/providers/sdk/chargebee/charge-bee.service.ts +++ b/src/providers/sdk/chargebee/charge-bee.service.ts @@ -1,8 +1,23 @@ /* eslint-disable @typescript-eslint/naming-convention */ import {inject} from '@loopback/core'; import chargebee from 'chargebee'; -import {Transaction} from '../../../types'; -import {CustomerAdapter, InvoiceAdapter, PaymentSourceAdapter} from './adapter'; +import { + CollectionMethod, + RecurringInterval, + TInvoicePrice, + TPrice, + TProduct, + TSubscriptionCreate, + TSubscriptionResult, + TSubscriptionUpdate, + Transaction, +} from '../../../types'; +import { + CustomerAdapter, + InvoiceAdapter, + PaymentSourceAdapter, + ChargebeeSubscriptionAdapter, +} from './adapter'; import {ChargeBeeBindings} from './key'; import { ChargeBeeConfig, @@ -16,18 +31,24 @@ export class ChargeBeeService implements IChargeBeeService { invoiceAdapter: InvoiceAdapter; customerAdapter: CustomerAdapter; paymentSource: PaymentSourceAdapter; + chargebeeSubscriptionAdapter: ChargebeeSubscriptionAdapter; constructor( @inject(ChargeBeeBindings.config, {optional: true}) private readonly chargeBeeConfig: ChargeBeeConfig, ) { - // config initialise - chargebee.configure({ - site: chargeBeeConfig.site, - api_key: chargeBeeConfig.apiKey, - }); + // Only configure the global chargebee singleton when a valid site is + // provided. This prevents a second instantiation with empty config + // (e.g. SDKProvider vs SubscriptionProvider) from resetting the site. + if (chargeBeeConfig?.site) { + chargebee.configure({ + site: chargeBeeConfig.site, + api_key: chargeBeeConfig.apiKey, + }); + } this.invoiceAdapter = new InvoiceAdapter(); this.customerAdapter = new CustomerAdapter(); this.paymentSource = new PaymentSourceAdapter(); + this.chargebeeSubscriptionAdapter = new ChargebeeSubscriptionAdapter(); } async createCustomer( customerDto: IChargeBeeCustomer, @@ -272,4 +293,322 @@ export class ChargeBeeService implements IChargeBeeService { throw new Error(JSON.stringify(error)); } } + + // --------------------------------------------------------------------------- + // ISubscriptionService implementation (Chargebee Items API v2) + // --------------------------------------------------------------------------- + + /** + * Creates a plan-type Item in Chargebee and returns its Item ID. + * + * Chargebee's equivalent of a Stripe Product is an `Item` with `type: plan`. + * + * @param product - Product details (name, optional description and metadata). + * @returns The Chargebee Item ID. + */ + async createProduct(product: TProduct): Promise { + try { + const itemId = product.name + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, ''); + + // Strip item_family_id from metadata — it is a top-level Chargebee param, + // and Chargebee rejects it if it appears inside metadata. + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const {item_family_id: _ignored, ...restMetadata} = (product.metadata ?? + {}) as Record; + const hasExtraMetadata = Object.keys(restMetadata).length > 0; + + const result = await chargebee.item + .create({ + id: itemId, + name: product.name, + description: product.description, + type: 'plan', + item_family_id: + (product.metadata?.['item_family_id'] as string) ?? + this.chargeBeeConfig.defaultItemFamilyId ?? + 'default', + // Only include metadata key if there are non-family fields to send. + // Passing metadata: undefined still serialises the key; use spread instead. + ...(hasExtraMetadata ? {metadata: restMetadata as object} : {}), + }) + .request(); + return result.item.id; + } catch (error) { + throw new Error(JSON.stringify(error)); + } + } + + /** + * Creates an ItemPrice (recurring price configuration) in Chargebee and + * returns the normalised {@link TPrice}. + * + * Chargebee's equivalent of a Stripe Price is an `ItemPrice`. + * + * @param price - Price configuration including currency, amount and recurrence. + * @returns The created price with its Chargebee-assigned ID. + */ + async createPrice(price: TPrice): Promise { + try { + // Chargebee requires an explicit ItemPrice ID or auto-generates one. + const priceId = + price.id ?? `${price.product}-${price.currency}-${Date.now()}`; + + const result = await chargebee.item_price + .create({ + id: priceId, + name: priceId, // Chargebee requires a display name + item_id: price.product, + currency_code: price.currency.toUpperCase(), + price: price.unitAmount, + pricing_model: (this.chargeBeeConfig.defaultPricingModel ?? + 'flat_fee') as + | 'flat_fee' + | 'per_unit' + | 'tiered' + | 'volume' + | 'stairstep', + period_unit: price.recurring?.interval as + | 'day' + | 'week' + | 'month' + | 'year' + | undefined, + period: price.recurring?.intervalCount, + tax_providers_fields: [], // Required by SDK type but can be empty + }) + .request(); + + const ip = result.item_price; + return { + id: ip.id, + currency: ip.currency_code.toLowerCase(), + unitAmount: ip.price ?? 0, + product: ip.item_id ?? price.product, + recurring: ip.period_unit + ? { + interval: ip.period_unit as RecurringInterval, + intervalCount: ip.period ?? 1, + } + : undefined, + active: ip.status === 'active', + }; + } catch (error) { + throw new Error(JSON.stringify(error)); + } + } + + /** + * Creates a new recurring subscription in Chargebee using the Items API. + * + * Uses `create_with_items` which maps to your `priceRefId` (ItemPrice ID). + * + * @param subscription - Subscription parameters. + * @returns The Chargebee Subscription ID. + */ + async createSubscription(subscription: TSubscriptionCreate): Promise { + try { + const result = await chargebee.subscription + .create_with_items(subscription.customerId, { + subscription_items: [ + { + item_price_id: subscription.priceRefId, + }, + ], + discounts: [], // Required by Chargebee SDK type + ...(subscription.collectionMethod === CollectionMethod.SEND_INVOICE + ? { + auto_collection: 'off' as const, + // Only include net_term_days if explicitly provided and site has payment terms configured + ...(subscription.daysUntilDue !== undefined + ? {net_term_days: subscription.daysUntilDue} + : {}), + } + : {auto_collection: 'on' as const}), + }) + .request(); + return result.subscription.id; + } catch (error) { + throw new Error(JSON.stringify(error)); + } + } + + /** + * Retrieves the current state of a subscription from Chargebee. + * + * @param subscriptionId - The Chargebee subscription ID. + * @returns A normalised {@link TSubscriptionResult}. + */ + async getSubscription(subscriptionId: string): Promise { + try { + const result = await chargebee.subscription + .retrieve(subscriptionId) + .request(); + return this.chargebeeSubscriptionAdapter.adaptToModel( + result.subscription, + ); + } catch (error) { + throw new Error(JSON.stringify(error)); + } + } + + /** + * Upgrades or downgrades an active subscription in Chargebee. + * + * Uses `update_for_items` which applies immediate proration by default. + * Pass `prorationBehavior: 'none'` in `updates` to suppress proration. + * + * @param subscriptionId - The Chargebee subscription ID to modify. + * @param updates - The new ItemPrice ID and optional proration behaviour. + * @returns A normalised {@link TSubscriptionResult} reflecting the change. + */ + async updateSubscription( + subscriptionId: string, + updates: TSubscriptionUpdate, + ): Promise { + try { + const result = await chargebee.subscription + .update_for_items(subscriptionId, { + subscription_items: updates.priceRefId + ? [{item_price_id: updates.priceRefId}] + : [], + discounts: [], // Required by Chargebee SDK type + // When prorationBehavior is 'none', pass prorate:false to suppress credit notes + prorate: updates.prorationBehavior !== 'none', + }) + .request(); + return this.chargebeeSubscriptionAdapter.adaptToModel( + result.subscription, + ); + } catch (error) { + throw new Error(JSON.stringify(error)); + } + } + + /** + * Cancels a subscription immediately in Chargebee. + * + * Sets `end_of_term: false` for immediate cancellation with proration credit + * (Chargebee applies a pro-rated credit note automatically). + * + * @param subscriptionId - The Chargebee subscription ID to cancel. + */ + async cancelSubscription(subscriptionId: string): Promise { + try { + await chargebee.subscription + .cancel_for_items(subscriptionId, { + end_of_term: this.chargeBeeConfig.cancelAtEndOfTerm ?? false, + cancel_reason_code: + this.chargeBeeConfig.defaultCancelReasonCode ?? 'customer_request', + }) + .request(); + } catch (error) { + throw new Error(JSON.stringify(error)); + } + } + + /** + * Pauses a subscription in Chargebee. + * + * The subscription moves to `paused` state; Chargebee stops generating + * invoices until the subscription is resumed. + * + * @param subscriptionId - The Chargebee subscription ID to pause. + */ + async pauseSubscription(subscriptionId: string): Promise { + try { + await chargebee.subscription.pause(subscriptionId, {}).request(); + } catch (error) { + throw new Error(JSON.stringify(error)); + } + } + + /** + * Resumes a previously paused subscription in Chargebee. + * + * @param subscriptionId - The Chargebee subscription ID to resume. + */ + async resumeSubscription(subscriptionId: string): Promise { + try { + await chargebee.subscription.resume(subscriptionId, {}).request(); + } catch (error) { + throw new Error(JSON.stringify(error)); + } + } + + /** + * Returns a detailed price breakdown for a Chargebee invoice including + * tax and the amount excluding tax. + * + * Chargebee stores amounts as units (not cents), so no conversion needed. + * + * @param invoiceId - The Chargebee invoice ID. + * @returns {@link TInvoicePrice} with amounts in the invoice's currency unit. + */ + async getInvoicePriceDetails(invoiceId: string): Promise { + try { + const result = await chargebee.invoice.retrieve(invoiceId).request(); + const inv = result.invoice; + const taxAmount: number = inv.tax ?? 0; + const totalAmount: number = inv.total ?? 0; + + return { + currency: (inv.currency_code ?? '').toUpperCase(), + totalAmount, + taxAmount, + amountExcludingTax: totalAmount - taxAmount, + }; + } catch (error) { + throw new Error(JSON.stringify(error)); + } + } + + /** + * Sends a hosted payment page link for the given Chargebee invoice. + * + * Uses Chargebee's `collect_payment` with `payment_source_id` omitted, + * which results in the payment link being sent via the Chargebee notification + * configured on the site. + * + * @param invoiceId - The Chargebee invoice ID. + */ + async sendPaymentLink(invoiceId: string): Promise { + try { + // Using collect_payment without a payment_source triggers Chargebee to + // send the hosted payment page link to the customer by email + // (based on site notification settings). + await chargebee.invoice + .collect_payment(invoiceId, { + payment_source_id: undefined, + }) + .request(); + } catch (error) { + throw new Error(JSON.stringify(error)); + } + } + + /** + * Checks whether a plan-type Item exists and is active in Chargebee. + * + * @param productId - The Chargebee Item ID. + * @returns `true` if the Item is active, `false` if archived or not found. + */ + async checkProductExists(productId: string): Promise { + try { + const result = await chargebee.item.retrieve(productId).request(); + return result.item.status === 'active'; + } catch (error) { + // Chargebee throws a JSON error string with api_error_code for not-found + const message = JSON.stringify(error); + if ( + message.includes('resource_not_found') || + message.includes('not_found') + ) { + return false; + } + throw new Error(message); + } + } } diff --git a/src/providers/sdk/chargebee/type/chargebee-config.type.ts b/src/providers/sdk/chargebee/type/chargebee-config.type.ts new file mode 100644 index 0000000..5c5b16f --- /dev/null +++ b/src/providers/sdk/chargebee/type/chargebee-config.type.ts @@ -0,0 +1,38 @@ +/** + * Configuration for the Chargebee billing provider. + * + * All fields beyond `site` and `apiKey` are optional overrides — sensible + * defaults are applied when omitted so existing integrations require no changes. + */ +export interface ChargeBeeConfig { + site: string; + apiKey: string; + + /** + * The Chargebee Item Family ID that new Items (Products) are created under. + * Defaults to `'default'` which works for single-family Chargebee sites. + * Override for multi-family setups. + */ + defaultItemFamilyId?: string; + + /** + * Pricing model applied when creating ItemPrices. + * Defaults to `'flat_fee'` (single fixed recurring charge). + * Other Chargebee values: `'per_unit'`, `'tiered'`, `'volume'`, `'stairstep'`. + */ + defaultPricingModel?: string; + + /** + * When `true`, subscriptions are cancelled at the end of the current billing + * period (grace-period cancellation). When `false` (default), the + * cancellation is immediate with a prorated credit note applied. + */ + cancelAtEndOfTerm?: boolean; + + /** + * The cancel reason code sent to Chargebee when a subscription is cancelled. + * Defaults to `'customer_request'`. + * Must be one of the reason codes configured on your Chargebee site. + */ + defaultCancelReasonCode?: string; +} diff --git a/src/providers/sdk/chargebee/type/index.ts b/src/providers/sdk/chargebee/type/index.ts index e5656fc..81428ac 100644 --- a/src/providers/sdk/chargebee/type/index.ts +++ b/src/providers/sdk/chargebee/type/index.ts @@ -1,18 +1,27 @@ -import {IService, Transaction} from '../../../../types'; +import {IService, ISubscriptionService, Transaction} from '../../../../types'; import {IChargeBeeCustomer} from './customer.type'; import {IChargeBeeInvoice} from './invoice.type'; import {IChargeBeePaymentSource} from './payment-source.type'; -export interface ChargeBeeConfig { - site: string; - apiKey: string; -} export const BillingDBSourceName = 'BillingDB'; -export interface IChargeBeeService extends IService { - // No Change +/** + * Full Chargebee service interface combining one-time billing ({@link IService}) + * and recurring-subscription management ({@link ISubscriptionService}). + * + * All subscription methods map to Chargebee's Items/Item-Prices/Subscriptions API + * which is the current (v2) Chargebee data model — matching our generalised types: + * + * | Library type | Chargebee equivalent | + * |--------------------|---------------------------| + * | TProduct | Item (type: plan) | + * | TPrice | ItemPrice | + * | TSubscriptionCreate| Subscription (create_with_items) | + * | TSubscriptionUpdate| Subscription (update_for_items) | + * | TSubscriptionResult| Subscription object | + */ +export interface IChargeBeeService extends IService, ISubscriptionService { createCustomer(customerDto: IChargeBeeCustomer): Promise; - getCustomers(customerId: string): Promise; updateCustomerById( customerId: string, @@ -42,3 +51,4 @@ export interface IChargeBeeService extends IService { export * from './invoice.type'; export * from './payment-source.type'; export * from './customer.type'; +export * from './chargebee-config.type'; From 0a1ea487c6f62169704c6be071a5a897eeb24e5c Mon Sep 17 00:00:00 2001 From: Piyush Singh Gaur Date: Sun, 29 Mar 2026 21:15:11 +0530 Subject: [PATCH 4/7] refactor(chore): address PR review comments for type safety and error handling address PR review comments for type safety and error handling GH-21 --- src/providers/billing.provider.ts | 81 +------------------ .../chargebee/adapter/subscription.adapter.ts | 23 ++++-- .../sdk/chargebee/charge-bee.service.ts | 42 +++------- .../stripe/adapter/subscription.adapter.ts | 20 ++--- src/providers/sdk/stripe/stripe.service.ts | 29 ++++--- src/types.ts | 11 ++- 6 files changed, 71 insertions(+), 135 deletions(-) diff --git a/src/providers/billing.provider.ts b/src/providers/billing.provider.ts index 86f0518..189c6f5 100644 --- a/src/providers/billing.provider.ts +++ b/src/providers/billing.provider.ts @@ -1,14 +1,7 @@ import {inject, Provider} from '@loopback/core'; - import {HttpErrors} from '@loopback/rest'; import {BillingComponentBindings} from '../keys'; -import { - IService, - TCustomer, - TInvoice, - TPaymentSource, - Transaction, -} from '../types'; +import {IService} from '../types'; export class BillingProvider implements Provider { constructor( @@ -18,7 +11,7 @@ export class BillingProvider implements Provider { private readonly sdkProvider?: IService, ) {} - getProvider() { + getProvider(): IService { if (this.sdkProvider && this.restProvider) { throw new HttpErrors.NotAcceptable(); } else if (this.sdkProvider) { @@ -30,75 +23,7 @@ export class BillingProvider implements Provider { } } - async createCustomer(customerDto: TCustomer): Promise { - return this.getProvider().createCustomer(customerDto); - } - - async getCustomers(customerId: string): Promise { - return this.getProvider().getCustomers(customerId); - } - - async updateCustomerById( - tenantId: string, - customerDto: Partial, - ): Promise { - return this.getProvider().updateCustomerById(tenantId, customerDto); - } - - async deleteCustomer(customerId: string): Promise { - return this.getProvider().deleteCustomer(customerId); - } - - async createPaymentSource( - paymentDto: TPaymentSource, - ): Promise { - return this.getProvider().createPaymentSource(paymentDto); - } - - async applyPaymentSourceForInvoice( - invoiceId: string, - transaction: Transaction, - ): Promise { - return this.getProvider().applyPaymentSourceForInvoice( - invoiceId, - transaction, - ); - } - - async retrievePaymentSource( - paymentSourceId: string, - ): Promise { - return this.getProvider().retrievePaymentSource(paymentSourceId); - } - - async deletePaymentSource(paymentSourceId: string): Promise { - return this.getProvider().deletePaymentSource(paymentSourceId); - } - - async createInvoice(invoice: TInvoice): Promise { - return this.getProvider().createInvoice(invoice); - } - - async retrieveInvoice(invoiceId: string): Promise { - return this.getProvider().retrieveInvoice(invoiceId); - } - - async updateInvoice( - invoiceId: string, - invoice: Partial, - ): Promise { - return this.getProvider().updateInvoice(invoiceId, invoice); - } - - async deleteInvoice(invoiceId: string): Promise { - return this.getProvider().deleteInvoice(invoiceId); - } - - async getPaymentStatus(invoiceId: string): Promise { - return this.getProvider().getPaymentStatus(invoiceId); - } - - value() { + value(): IService { return this.getProvider(); } } diff --git a/src/providers/sdk/chargebee/adapter/subscription.adapter.ts b/src/providers/sdk/chargebee/adapter/subscription.adapter.ts index 8b87634..04bf426 100644 --- a/src/providers/sdk/chargebee/adapter/subscription.adapter.ts +++ b/src/providers/sdk/chargebee/adapter/subscription.adapter.ts @@ -5,6 +5,20 @@ import { TSubscriptionResult, } from '../../../../types'; +/** + * Local interface covering the Chargebee Subscription fields we map. + * Chargebee does not export a typed subscription object from its SDK, + * so we define the shape ourselves — no `any` needed. + */ +export interface RawChargebeeSubscription { + id: string; + status: string; + customer_id: string; + current_term_start?: number; + current_term_end?: number; + cancel_at_period_end?: boolean; +} + /** * Adapter that converts between the raw Chargebee Subscription object and the * provider-agnostic {@link TSubscriptionResult} shape used throughout the @@ -17,10 +31,9 @@ import { * @example * ```ts * class MyAdapter extends ChargebeeSubscriptionAdapter { - * adaptToModel(resp: unknown): TSubscriptionResult { + * adaptToModel(resp: RawChargebeeSubscription & {trial_end?: number}): TSubscriptionResult { * const base = super.adaptToModel(resp); - * const raw = resp as {trial_end?: number}; - * return {...base, trialEnd: raw.trial_end}; + * return {...base, trialEnd: resp.trial_end}; * } * } * // then: @@ -36,9 +49,7 @@ export class ChargebeeSubscriptionAdapter * * @param resp - Raw Chargebee Subscription returned by the SDK. */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - adaptToModel(resp: any): TSubscriptionResult { - // NOSONAR + adaptToModel(resp: RawChargebeeSubscription): TSubscriptionResult { return { id: resp.id, status: resp.status, diff --git a/src/providers/sdk/chargebee/charge-bee.service.ts b/src/providers/sdk/chargebee/charge-bee.service.ts index 9f4fbb2..f2215aa 100644 --- a/src/providers/sdk/chargebee/charge-bee.service.ts +++ b/src/providers/sdk/chargebee/charge-bee.service.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import {randomUUID} from 'crypto'; import {inject} from '@loopback/core'; import chargebee from 'chargebee'; import { - CollectionMethod, RecurringInterval, TInvoicePrice, TPrice, @@ -353,8 +353,11 @@ export class ChargeBeeService implements IChargeBeeService { async createPrice(price: TPrice): Promise { try { // Chargebee requires an explicit ItemPrice ID or auto-generates one. + // randomUUID() is cryptographically secure (Node.js built-in v14.17+); + // the first 8-hex segment keeps the ID short and Chargebee-friendly. const priceId = - price.id ?? `${price.product}-${price.currency}-${Date.now()}`; + price.id ?? + `${price.product}-${price.currency}-${randomUUID().split('-')[0]}`; const result = await chargebee.item_price .create({ @@ -410,24 +413,10 @@ export class ChargeBeeService implements IChargeBeeService { */ async createSubscription(subscription: TSubscriptionCreate): Promise { try { + const params = + this.chargebeeSubscriptionAdapter.adaptFromModel(subscription); const result = await chargebee.subscription - .create_with_items(subscription.customerId, { - subscription_items: [ - { - item_price_id: subscription.priceRefId, - }, - ], - discounts: [], // Required by Chargebee SDK type - ...(subscription.collectionMethod === CollectionMethod.SEND_INVOICE - ? { - auto_collection: 'off' as const, - // Only include net_term_days if explicitly provided and site has payment terms configured - ...(subscription.daysUntilDue !== undefined - ? {net_term_days: subscription.daysUntilDue} - : {}), - } - : {auto_collection: 'on' as const}), - }) + .create_with_items(subscription.customerId, params) .request(); return result.subscription.id; } catch (error) { @@ -579,11 +568,7 @@ export class ChargeBeeService implements IChargeBeeService { // Using collect_payment without a payment_source triggers Chargebee to // send the hosted payment page link to the customer by email // (based on site notification settings). - await chargebee.invoice - .collect_payment(invoiceId, { - payment_source_id: undefined, - }) - .request(); + await chargebee.invoice.collect_payment(invoiceId, {}).request(); } catch (error) { throw new Error(JSON.stringify(error)); } @@ -600,15 +585,14 @@ export class ChargeBeeService implements IChargeBeeService { const result = await chargebee.item.retrieve(productId).request(); return result.item.status === 'active'; } catch (error) { - // Chargebee throws a JSON error string with api_error_code for not-found - const message = JSON.stringify(error); + const cbError = error as {api_error_code?: string; http_status?: number}; if ( - message.includes('resource_not_found') || - message.includes('not_found') + cbError.api_error_code === 'resource_not_found' || + cbError.http_status === 404 ) { return false; } - throw new Error(message); + throw new Error(JSON.stringify(error)); } } } diff --git a/src/providers/sdk/stripe/adapter/subscription.adapter.ts b/src/providers/sdk/stripe/adapter/subscription.adapter.ts index fb12150..ac98459 100644 --- a/src/providers/sdk/stripe/adapter/subscription.adapter.ts +++ b/src/providers/sdk/stripe/adapter/subscription.adapter.ts @@ -33,17 +33,19 @@ export class StripeSubscriptionAdapter * * @param resp - Raw Stripe Subscription returned by the SDK. */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - adaptToModel(resp: any): TSubscriptionResult { - const sub = resp as Stripe.Subscription; + adaptToModel(resp: Stripe.Subscription): TSubscriptionResult { return { - id: sub.id, - status: sub.status, + id: resp.id, + status: resp.status, customerId: - typeof sub.customer === 'string' ? sub.customer : sub.customer?.id, - currentPeriodStart: sub.current_period_start, - currentPeriodEnd: sub.current_period_end, - cancelAtPeriodEnd: sub.cancel_at_period_end, + typeof resp.customer === 'string' + ? resp.customer + : resp.customer && 'id' in resp.customer + ? resp.customer.id + : undefined, + currentPeriodStart: resp.current_period_start, + currentPeriodEnd: resp.current_period_end, + cancelAtPeriodEnd: resp.cancel_at_period_end, }; } diff --git a/src/providers/sdk/stripe/stripe.service.ts b/src/providers/sdk/stripe/stripe.service.ts index 1bf2ce2..ae95f15 100644 --- a/src/providers/sdk/stripe/stripe.service.ts +++ b/src/providers/sdk/stripe/stripe.service.ts @@ -314,11 +314,9 @@ export class StripeService implements IStripeService { * @returns The Stripe subscription ID. */ async createSubscription(subscription: TSubscriptionCreate): Promise { + const params = this.stripeSubscriptionAdapter.adaptFromModel(subscription); const created = await this.stripe.subscriptions.create({ - customer: subscription.customerId, - items: [{price: subscription.priceRefId}], - collection_method: subscription.collectionMethod, - days_until_due: subscription.daysUntilDue, + ...params, payment_behavior: (this.stripeConfig.defaultPaymentBehavior ?? 'default_incomplete') as Stripe.SubscriptionCreateParams.PaymentBehavior, }); @@ -364,7 +362,9 @@ export class StripeService implements IStripeService { const newId = await this.createSubscription({ customerId: existing.customer as string, priceRefId: updates.priceRefId ?? '', - collectionMethod: CollectionMethod.CHARGE_AUTOMATICALLY, + collectionMethod: + updates.collectionMethod ?? CollectionMethod.CHARGE_AUTOMATICALLY, + daysUntilDue: updates.daysUntilDue, }); return { id: newId, @@ -413,10 +413,13 @@ export class StripeService implements IStripeService { }), ); } catch (err) { - // Non-fatal — subscription is already cancelled in Stripe; log for observability - console.info( - '[StripeService] cancelSubscription: invoice cleanup failed', - err, + // Invoice cleanup is best-effort after cancellation. + // Surface as a structured error so callers and APM tools can observe it. + throw Object.assign( + new Error( + `[StripeService] cancelSubscription: invoice cleanup failed for ${subscriptionId}`, + ), + {cause: err}, ); } } @@ -440,8 +443,12 @@ export class StripeService implements IStripeService { */ async resumeSubscription(subscriptionId: string): Promise { await this.stripe.subscriptions.update(subscriptionId, { - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - pause_collection: '' as any, // NOSONAR – Stripe uses empty string to clear pause_collection + // Stripe clears pause_collection by passing an empty string. + // The SDK types do not model this; cast through unknown to preserve + // intent without using any. + // Ref: https://stripe.com/docs/billing/subscriptions/pause-payment + pause_collection: + '' as unknown as Stripe.SubscriptionUpdateParams.PauseCollection, }); } diff --git a/src/types.ts b/src/types.ts index 71b77f2..1d01941 100644 --- a/src/types.ts +++ b/src/types.ts @@ -195,6 +195,10 @@ export interface TSubscriptionUpdate { /** New price / plan reference ID. */ priceRefId?: string; prorationBehavior?: ProrationBehavior; + /** Billing collection method to use when re-creating an incomplete subscription. */ + collectionMethod?: CollectionMethod; + /** Number of days until the invoice is due (applicable for send_invoice). */ + daysUntilDue?: number; } /** @@ -203,7 +207,8 @@ export interface TSubscriptionUpdate { export interface TSubscriptionResult { id: string; status: string; - customerId: string; + /** Optional — customer may be deleted or unexpanded by the provider. */ + customerId?: string; currentPeriodStart?: number; currentPeriodEnd?: number; cancelAtPeriodEnd?: boolean; @@ -257,7 +262,9 @@ export interface ISubscriptionService { ): Promise; /** - * Cancels a subscription immediately with proration and voids open invoices. + * Cancels a subscription. Providers may apply proration, credit notes, or + * invoice voiding automatically based on their own billing rules. + * After this call the subscription will no longer renew. */ cancelSubscription(subscriptionId: string): Promise; From 799fd294f0d692613ccfefc339bbd19ca94991d9 Mon Sep 17 00:00:00 2001 From: Piyush Singh Gaur Date: Mon, 30 Mar 2026 15:15:46 +0530 Subject: [PATCH 5/7] fix(provider): fix sonar issues fix sonar issue GH-0 --- .../sdk/chargebee/charge-bee.service.ts | 29 ++++++++----------- .../chargebee/type/chargebee-config.type.ts | 9 ++++++ .../stripe/adapter/subscription.adapter.ts | 27 +++++++++++++---- tsconfig.json | 1 + 4 files changed, 43 insertions(+), 23 deletions(-) diff --git a/src/providers/sdk/chargebee/charge-bee.service.ts b/src/providers/sdk/chargebee/charge-bee.service.ts index f2215aa..7a0527e 100644 --- a/src/providers/sdk/chargebee/charge-bee.service.ts +++ b/src/providers/sdk/chargebee/charge-bee.service.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import {randomUUID} from 'crypto'; +import {randomUUID} from 'node:crypto'; import {inject} from '@loopback/core'; import chargebee from 'chargebee'; import { @@ -21,6 +21,8 @@ import { import {ChargeBeeBindings} from './key'; import { ChargeBeeConfig, + ChargebeePeriodUnit, + ChargebeePricingModel, IChargeBeeCustomer, IChargeBeeInvoice, IChargeBeePaymentSource, @@ -310,14 +312,14 @@ export class ChargeBeeService implements IChargeBeeService { try { const itemId = product.name .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-|-$/g, ''); + .replaceAll(/[^a-z0-9]+/g, '-') + .replaceAll(/^-|-$/g, ''); // Strip item_family_id from metadata — it is a top-level Chargebee param, // and Chargebee rejects it if it appears inside metadata. // eslint-disable-next-line @typescript-eslint/no-unused-vars - const {item_family_id: _ignored, ...restMetadata} = (product.metadata ?? - {}) as Record; + const {item_family_id: _ignored, ...restMetadata} = + product.metadata ?? {}; const hasExtraMetadata = Object.keys(restMetadata).length > 0; const result = await chargebee.item @@ -327,7 +329,7 @@ export class ChargeBeeService implements IChargeBeeService { description: product.description, type: 'plan', item_family_id: - (product.metadata?.['item_family_id'] as string) ?? + product.metadata?.['item_family_id'] ?? this.chargeBeeConfig.defaultItemFamilyId ?? 'default', // Only include metadata key if there are non-family fields to send. @@ -367,17 +369,9 @@ export class ChargeBeeService implements IChargeBeeService { currency_code: price.currency.toUpperCase(), price: price.unitAmount, pricing_model: (this.chargeBeeConfig.defaultPricingModel ?? - 'flat_fee') as - | 'flat_fee' - | 'per_unit' - | 'tiered' - | 'volume' - | 'stairstep', + 'flat_fee') as ChargebeePricingModel, period_unit: price.recurring?.interval as - | 'day' - | 'week' - | 'month' - | 'year' + | ChargebeePeriodUnit | undefined, period: price.recurring?.intervalCount, tax_providers_fields: [], // Required by SDK type but can be empty @@ -585,10 +579,11 @@ export class ChargeBeeService implements IChargeBeeService { const result = await chargebee.item.retrieve(productId).request(); return result.item.status === 'active'; } catch (error) { + const HTTP_NOT_FOUND = 404; const cbError = error as {api_error_code?: string; http_status?: number}; if ( cbError.api_error_code === 'resource_not_found' || - cbError.http_status === 404 + cbError.http_status === HTTP_NOT_FOUND ) { return false; } diff --git a/src/providers/sdk/chargebee/type/chargebee-config.type.ts b/src/providers/sdk/chargebee/type/chargebee-config.type.ts index 5c5b16f..0ba4522 100644 --- a/src/providers/sdk/chargebee/type/chargebee-config.type.ts +++ b/src/providers/sdk/chargebee/type/chargebee-config.type.ts @@ -36,3 +36,12 @@ export interface ChargeBeeConfig { */ defaultCancelReasonCode?: string; } + +export type ChargebeePricingModel = + | 'flat_fee' + | 'per_unit' + | 'tiered' + | 'volume' + | 'stairstep'; + +export type ChargebeePeriodUnit = 'day' | 'week' | 'month' | 'year'; diff --git a/src/providers/sdk/stripe/adapter/subscription.adapter.ts b/src/providers/sdk/stripe/adapter/subscription.adapter.ts index ac98459..5ed935c 100644 --- a/src/providers/sdk/stripe/adapter/subscription.adapter.ts +++ b/src/providers/sdk/stripe/adapter/subscription.adapter.ts @@ -34,15 +34,11 @@ export class StripeSubscriptionAdapter * @param resp - Raw Stripe Subscription returned by the SDK. */ adaptToModel(resp: Stripe.Subscription): TSubscriptionResult { + const customerId = this.extractCustomerId(resp.customer); return { id: resp.id, status: resp.status, - customerId: - typeof resp.customer === 'string' - ? resp.customer - : resp.customer && 'id' in resp.customer - ? resp.customer.id - : undefined, + customerId, currentPeriodStart: resp.current_period_start, currentPeriodEnd: resp.current_period_end, cancelAtPeriodEnd: resp.cancel_at_period_end, @@ -66,4 +62,23 @@ export class StripeSubscriptionAdapter }), }; } + + /** + * Extracts customer ID from Stripe customer field. + * Handles string ID, expanded Customer object, and DeletedCustomer edge case. + * + * @param customer - Stripe customer field (string | Customer | DeletedCustomer) + * @returns Customer ID or undefined if customer was deleted + */ + private extractCustomerId( + customer: string | Stripe.Customer | Stripe.DeletedCustomer, + ): string | undefined { + if (typeof customer === 'string') { + return customer; + } + if (customer && 'id' in customer) { + return customer.id; + } + return undefined; + } } diff --git a/tsconfig.json b/tsconfig.json index b3e68fb..f823e5d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "$schema": "http://json.schemastore.org/tsconfig", "extends": "@loopback/build/config/tsconfig.common.json", "compilerOptions": { + "lib": ["es2021"], "typeRoots": ["./src/typing", "./node_modules/@types"], "rootDir": "src", "outDir": "dist", From a23c7d8ffe9a5114de31565202470051f0e5af24 Mon Sep 17 00:00:00 2001 From: Piyush Singh Gaur Date: Mon, 30 Mar 2026 17:12:13 +0530 Subject: [PATCH 6/7] fix: resolve runtime bugs found during sandbox e2e testing - chargebee: guard invoice.options?.autoCollection against undefined - stripe: fix createInvoice to skip shipping_details when not provided - stripe: fix line2 concatenation (undefined undefined) bug - stripe: buildShippingDetails returns undefined for empty name - stripe: fix autoAdvnace typo to autoAdvance across service/adapter/type GH-1 --- .../sdk/chargebee/charge-bee.service.ts | 4 +- .../sdk/stripe/adapter/invoice.adapter.ts | 4 +- src/providers/sdk/stripe/stripe.service.ts | 53 ++++++++++++------- src/providers/sdk/stripe/type/invoice.type.ts | 2 +- 4 files changed, 39 insertions(+), 24 deletions(-) diff --git a/src/providers/sdk/chargebee/charge-bee.service.ts b/src/providers/sdk/chargebee/charge-bee.service.ts index 7a0527e..50916bf 100644 --- a/src/providers/sdk/chargebee/charge-bee.service.ts +++ b/src/providers/sdk/chargebee/charge-bee.service.ts @@ -233,9 +233,9 @@ export class ChargeBeeService implements IChargeBeeService { country: invoice.shippingAddress?.country, }, charges: invoice.charges, - auto_collection: invoice.options.autoCollection, + auto_collection: invoice.options?.autoCollection, discounts: - invoice.options.discounts?.map(discount => ({ + invoice.options?.discounts?.map(discount => ({ ...discount, apply_on: discount.applyOn, // Convert to snake_case })) ?? [], diff --git a/src/providers/sdk/stripe/adapter/invoice.adapter.ts b/src/providers/sdk/stripe/adapter/invoice.adapter.ts index d12e732..97d19c9 100644 --- a/src/providers/sdk/stripe/adapter/invoice.adapter.ts +++ b/src/providers/sdk/stripe/adapter/invoice.adapter.ts @@ -35,7 +35,7 @@ export class StripeInvoiceAdapter implements IAdapter { }), ), options: { - autoAdvnace: resp.auto_advance || false, + autoAdvance: resp.auto_advance || false, }, }; } @@ -66,7 +66,7 @@ export class StripeInvoiceAdapter implements IAdapter { customer: data.customerId, currency: data.currencyCode, ...shippingDetails, - auto_advance: data.options?.autoAdvnace ?? false, + auto_advance: data.options?.autoAdvance ?? false, }; } } diff --git a/src/providers/sdk/stripe/stripe.service.ts b/src/providers/sdk/stripe/stripe.service.ts index ae95f15..90d3429 100644 --- a/src/providers/sdk/stripe/stripe.service.ts +++ b/src/providers/sdk/stripe/stripe.service.ts @@ -160,24 +160,35 @@ export class StripeService implements IStripeService { } async createInvoice(invoice: IStripeInvoice): Promise { - const createdInvoice = await this.stripe.invoices.create({ + const addr = invoice.shippingAddress; + const shippingName = addr + ? [addr.firstName ?? '', addr.lastName ?? ''].join(' ').trim() + : ''; + + const createParams: Stripe.InvoiceCreateParams = { customer: invoice.customerId, - auto_advance: invoice.options?.autoAdvnace ?? false, // Optional - shipping_details: { - address: { - city: invoice.shippingAddress?.city, - country: invoice.shippingAddress?.country, - line1: invoice.shippingAddress?.line1, - line2: - invoice.shippingAddress?.line2 + - ' ' + - invoice.shippingAddress?.line3, - postal_code: invoice.shippingAddress?.zip, - state: invoice.shippingAddress?.state, - }, - name: invoice.customerId, - }, - }); + auto_advance: invoice.options?.autoAdvance ?? false, + ...(addr && shippingName + ? { + shipping_details: { + name: shippingName, + address: { + city: addr.city, + country: addr.country, + line1: addr.line1, + line2: + [addr.line2, addr.line3].filter(Boolean).join(' ') || + undefined, + postal_code: addr.zip, + state: addr.state, + }, + phone: addr.phone, + }, + } + : {}), + }; + + const createdInvoice = await this.stripe.invoices.create(createParams); // First, create invoice items for the customer for (const lineItem of invoice.charges ?? []) { // Assuming items is an array in TInvoice @@ -206,9 +217,12 @@ export class StripeService implements IStripeService { ): Promise { const updateData: Stripe.InvoiceUpdateParams = {}; if (invoice.shippingAddress) { - updateData.shipping_details = this.buildShippingDetails( + const shippingDetails = this.buildShippingDetails( invoice.shippingAddress, ); + if (shippingDetails) { + updateData.shipping_details = shippingDetails; + } } const updatedInvoice = await this.stripe.invoices.update( invoiceId, @@ -219,8 +233,9 @@ export class StripeService implements IStripeService { private buildShippingDetails( addr: IStripeInvoice['shippingAddress'], - ): Stripe.InvoiceUpdateParams.ShippingDetails { + ): Stripe.InvoiceUpdateParams.ShippingDetails | undefined { const name = [addr?.firstName ?? '', addr?.lastName ?? ''].join(' ').trim(); + if (!name) return undefined; return { name, address: { diff --git a/src/providers/sdk/stripe/type/invoice.type.ts b/src/providers/sdk/stripe/type/invoice.type.ts index 5e4fd1d..f0b0bb8 100644 --- a/src/providers/sdk/stripe/type/invoice.type.ts +++ b/src/providers/sdk/stripe/type/invoice.type.ts @@ -4,6 +4,6 @@ import {IAddressDto} from '../../chargebee'; export interface IStripeInvoice extends TInvoice { shippingAddress: IAddressDto | undefined; options?: { - autoAdvnace?: boolean; + autoAdvance?: boolean; }; } From 7656781ad43ec0daf322defb1a6c7cf20671fd3e Mon Sep 17 00:00:00 2001 From: Piyush Singh Gaur Date: Thu, 2 Apr 2026 12:36:45 +0530 Subject: [PATCH 7/7] refactor(chore): address PR review comments --- package-lock.json | 1529 +++++++++-------- .../chargebee/adapter/subscription.adapter.ts | 17 +- .../sdk/chargebee/charge-bee.service.ts | 35 +- .../stripe/adapter/subscription.adapter.ts | 12 +- src/providers/sdk/stripe/stripe.service.ts | 21 +- .../sdk/stripe/type/stripe-config.type.ts | 3 +- 6 files changed, 864 insertions(+), 753 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9eec7b3..c326261 100644 --- a/package-lock.json +++ b/package-lock.json @@ -689,9 +689,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -706,9 +706,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -724,9 +724,9 @@ "license": "MIT" }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -818,9 +818,9 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -829,9 +829,9 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -863,27 +863,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1016,9 +995,9 @@ } }, "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -1149,9 +1128,9 @@ "license": "MIT" }, "node_modules/@loopback/build": { - "version": "12.0.4", - "resolved": "https://registry.npmjs.org/@loopback/build/-/build-12.0.4.tgz", - "integrity": "sha512-qvlakCHtfm8W/hbXyQ0v8yZ6nk16KcKUaVJ3QNA3dpGGxmLwn72UVQ2KcTE6GMntTE9As3O7IybRVjy9HZoceQ==", + "version": "12.0.10", + "resolved": "https://registry.npmjs.org/@loopback/build/-/build-12.0.10.tgz", + "integrity": "sha512-KnFFhIr7jdHwLfVA5v5m3n5j0rz7bAI2VztIRx1HQzvzfWnlrG10JlxNb3JoAvUt53ckAy2svhJ8FazAqa7djw==", "dev": true, "license": "MIT", "dependencies": { @@ -1159,14 +1138,14 @@ "@types/mocha": "^10.0.10", "@types/node": "^16.18.126", "cross-spawn": "^7.0.6", - "debug": "^4.4.1", + "debug": "^4.4.3", "eslint": "^8.57.1", - "fs-extra": "^11.3.1", - "glob": "^11.0.3", - "lodash": "^4.17.21", - "mocha": "^11.7.2", + "fs-extra": "^11.3.4", + "glob": "^11.1.0", + "lodash": "^4.17.23", + "mocha": "^11.7.5", "nyc": "^17.1.0", - "prettier": "^3.6.2", + "prettier": "^3.8.1", "rimraf": "^5.0.10", "source-map-support": "^0.5.21", "typescript": "~5.2.2" @@ -1191,6 +1170,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@loopback/build/node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, "node_modules/@loopback/context": { "version": "8.0.4", "resolved": "https://registry.npmjs.org/@loopback/context/-/context-8.0.4.tgz", @@ -1244,20 +1230,20 @@ } }, "node_modules/@loopback/express": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/@loopback/express/-/express-8.0.4.tgz", - "integrity": "sha512-YV7n3gLwT/ZpBFuzNpkpkO63uDgp/InzPqNPFKJE02QGYu5wd88sfMune8haevLzRptHqvII5gIAavtOTtQyTQ==", + "version": "8.0.10", + "resolved": "https://registry.npmjs.org/@loopback/express/-/express-8.0.10.tgz", + "integrity": "sha512-/ujbHyN2GlW6oZsarwtQ+1T/BpfShnwMPLR3HaG4vw4cOSvGQlSgwPVySShDZHOJ4onkYzbT28+JaowxCqHVEg==", "license": "MIT", "dependencies": { - "@loopback/http-server": "^7.0.4", + "@loopback/http-server": "^7.0.10", "@types/body-parser": "^1.19.6", - "@types/express": "^4.17.23", - "@types/express-serve-static-core": "^4.17.37", + "@types/express": "^4.17.25", + "@types/express-serve-static-core": "^4.19.8", "@types/http-errors": "^2.0.5", - "body-parser": "^2.2.0", - "debug": "^4.4.1", - "express": "^4.21.2", - "http-errors": "^2.0.0", + "body-parser": "^2.2.2", + "debug": "^4.4.3", + "express": "^4.22.1", + "http-errors": "^2.0.1", "on-finished": "^2.4.1", "toposort": "^2.0.2", "tslib": "^2.8.1" @@ -1269,10 +1255,39 @@ "@loopback/core": "^7.0.0" } }, + "node_modules/@loopback/express/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@loopback/express/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/@loopback/filter": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@loopback/filter/-/filter-6.0.4.tgz", - "integrity": "sha512-RjCdyIG9bKFbi4OWWOL1kH2c1vpF+o6jWVgh0J32h88rmQQpXE0qoDhilRK3Z880wRAizMv4V8UHB6hYLAIGhg==", + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/@loopback/filter/-/filter-6.0.10.tgz", + "integrity": "sha512-4ZaUwpxRiPfZO9uEwh/prqDLbgyBlk+WNSKWLj7xCT3w27KA7l5jKqxA4ICn+98iEdlF2YsczONFIxxxu4GSqg==", "license": "MIT", "peer": true, "dependencies": { @@ -1283,12 +1298,12 @@ } }, "node_modules/@loopback/http-server": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/@loopback/http-server/-/http-server-7.0.4.tgz", - "integrity": "sha512-cp8M0b7P7Bvx6eP3dMPL6Yd/N3/M1Z8//JkKQpdJ5Jqe1xLWtCzhA8o5Sc1Yhnb9wB0Hn0bAMP4INggkZmzCvA==", + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/@loopback/http-server/-/http-server-7.0.10.tgz", + "integrity": "sha512-Wl62Ene47qgiq9k3zccbHy/AVQkr0MjHjZhxME9KIh6K1PiJZaBxRktaNPiucr2jgJIeKFCEcjkqNyd+FR92eg==", "license": "MIT", "dependencies": { - "debug": "^4.4.1", + "debug": "^4.4.3", "stoppable": "^1.1.0", "tslib": "^2.8.1" }, @@ -1312,16 +1327,16 @@ } }, "node_modules/@loopback/openapi-v3": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/@loopback/openapi-v3/-/openapi-v3-11.0.5.tgz", - "integrity": "sha512-fLTD9VrYCB8A+ZBTyOgkSLsHqX8HZMa+U70EC9P0U59j9OerkN/bdRFM/eotx0vsuXfbHYCHCLps1athuv791w==", + "version": "11.0.11", + "resolved": "https://registry.npmjs.org/@loopback/openapi-v3/-/openapi-v3-11.0.11.tgz", + "integrity": "sha512-sWXzDp9pLPciZZxyClVbxED9qQ7RpymwcWAzdNjF1zDqYVB4gendOPHf2ycZYwaKrXgPF6chXGRtAjMxiiDMTQ==", "license": "MIT", "dependencies": { - "@loopback/repository-json-schema": "^9.0.5", - "debug": "^4.4.1", + "@loopback/repository-json-schema": "^9.0.11", + "debug": "^4.4.3", "http-status": "^1.8.1", "json-merge-patch": "^1.0.2", - "lodash": "^4.17.21", + "lodash": "^4.17.23", "openapi3-ts": "^2.0.2", "tslib": "^2.8.1" }, @@ -1332,18 +1347,24 @@ "@loopback/core": "^7.0.0" } }, + "node_modules/@loopback/openapi-v3/node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, "node_modules/@loopback/repository": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/@loopback/repository/-/repository-8.0.4.tgz", - "integrity": "sha512-3Vl0R+3iE9TS9+w0P3NBhkNEh4+ni7vIlBrAEQ8YjEUrk4X8nhNcJaZLOsaZ8r96/ahZylXmK9KtdblkHtDOMg==", + "version": "8.0.10", + "resolved": "https://registry.npmjs.org/@loopback/repository/-/repository-8.0.10.tgz", + "integrity": "sha512-ejUlDhZHMpEBgjYpB8KpYXVklw6emMIs7AAi5RUW9iL/Ve97acUqQxBuXsmKepcGDxhzeyEtDdAquS3hglmomA==", "license": "MIT", "peer": true, "dependencies": { - "@loopback/filter": "^6.0.4", + "@loopback/filter": "^6.0.10", "@types/debug": "^4.1.12", - "debug": "^4.4.1", - "lodash": "^4.17.21", - "loopback-datasource-juggler": "^5.2.1", + "debug": "^4.4.3", + "lodash": "^4.17.23", + "loopback-datasource-juggler": "^6.0.4", "tslib": "^2.8.1" }, "engines": { @@ -1354,13 +1375,13 @@ } }, "node_modules/@loopback/repository-json-schema": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/@loopback/repository-json-schema/-/repository-json-schema-9.0.5.tgz", - "integrity": "sha512-QSsXHokFE+GTY5Kl5aKetrPwcOuh77qsSv9sLjiTlSJjDQvxxZnNyetee73mDNtOB/zjFDfRYqoynhYBVD+eCw==", + "version": "9.0.11", + "resolved": "https://registry.npmjs.org/@loopback/repository-json-schema/-/repository-json-schema-9.0.11.tgz", + "integrity": "sha512-9duus58oOTUxEf+khjVTgn8XMXNBXHy+kGwfCFlpdggjXemfcS0th5nArSHcFd1LPVGGcwhrPQq0VBKi9TdqYA==", "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.15", - "debug": "^4.4.1", + "debug": "^4.4.3", "tslib": "^2.8.1" }, "engines": { @@ -1371,43 +1392,50 @@ "@loopback/repository": "^8.0.1" } }, + "node_modules/@loopback/repository/node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT", + "peer": true + }, "node_modules/@loopback/rest": { - "version": "15.0.5", - "resolved": "https://registry.npmjs.org/@loopback/rest/-/rest-15.0.5.tgz", - "integrity": "sha512-gOr7xJ5SvDruyt955+H1UswANETRE7d5lyfWFZ7ETVsqJ3Yl3bKyGAJ7gR/twKO2WWtr5pe6wavC48zkMI/1og==", + "version": "15.0.11", + "resolved": "https://registry.npmjs.org/@loopback/rest/-/rest-15.0.11.tgz", + "integrity": "sha512-HViVX4EVsN8fI8kggcEMhxUWJ3UWIrn5T++c/uNiKHSvy77TUIG2aA8JTOVDlos8wZJ8NNOrSmCtWQ6fcavbTw==", "license": "MIT", "dependencies": { - "@loopback/express": "^8.0.4", - "@loopback/http-server": "^7.0.4", - "@loopback/openapi-v3": "^11.0.5", + "@loopback/express": "^8.0.10", + "@loopback/http-server": "^7.0.10", + "@loopback/openapi-v3": "^11.0.11", "@openapi-contrib/openapi-schema-to-json-schema": "^5.1.0", "@types/body-parser": "^1.19.6", "@types/cors": "^2.8.19", - "@types/express": "^4.17.23", - "@types/express-serve-static-core": "^4.17.37", + "@types/express": "^4.17.25", + "@types/express-serve-static-core": "^4.19.8", "@types/http-errors": "^2.0.5", "@types/on-finished": "^2.3.5", - "@types/serve-static": "1.15.8", + "@types/serve-static": "2.2.0", "@types/type-is": "^1.6.7", - "ajv": "^8.17.1", + "ajv": "^8.18.0", "ajv-errors": "^3.0.0", "ajv-formats": "^3.0.1", "ajv-keywords": "^5.1.0", - "body-parser": "^2.2.0", - "cors": "^2.8.5", - "debug": "^4.4.1", - "express": "^4.21.2", - "http-errors": "^2.0.0", - "js-yaml": "^4.1.0", + "body-parser": "^2.2.2", + "cors": "^2.8.6", + "debug": "^4.4.3", + "express": "^4.22.1", + "http-errors": "^2.0.1", + "js-yaml": "^4.1.1", "json-schema-compare": "^0.2.2", - "lodash": "^4.17.21", + "lodash": "^4.17.23", "on-finished": "^2.4.1", "path-to-regexp": "^6.3.0", - "qs": "^6.14.0", - "strong-error-handler": "^5.0.23", + "qs": "^6.15.0", + "strong-error-handler": "^5.0.29", "tslib": "^2.8.1", "type-is": "^2.0.1", - "validator": "^13.15.15" + "validator": "^13.15.26" }, "engines": { "node": "20 || 22 || 24" @@ -1417,13 +1445,13 @@ } }, "node_modules/@loopback/rest-explorer": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/@loopback/rest-explorer/-/rest-explorer-8.0.5.tgz", - "integrity": "sha512-hgSh90ZM/SDFT8uWgCEzKEMLXwjqtxO6ympiQ/mVt2WOCs6w0SKcTMaP7l/vrEd4QVTCHtF1LcJGtkngcxb+Vw==", + "version": "8.0.11", + "resolved": "https://registry.npmjs.org/@loopback/rest-explorer/-/rest-explorer-8.0.11.tgz", + "integrity": "sha512-IUi6LJjeNx8nyyaevnBGwPqRZAeKhkrAia2m9YXby2klkG2BgLHFiX1br3ZQwT0RzmUqvZMt82DFW2nnqo4CEg==", "license": "MIT", "dependencies": { "ejs": "^3.1.10", - "swagger-ui-dist": "5.28.1", + "swagger-ui-dist": "5.31.0", "tslib": "^2.8.1" }, "engines": { @@ -1434,6 +1462,51 @@ "@loopback/rest": "^15.0.1" } }, + "node_modules/@loopback/rest/node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@loopback/rest/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@loopback/rest/node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, + "node_modules/@loopback/rest/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/@loopback/testlab": { "version": "8.0.4", "resolved": "https://registry.npmjs.org/@loopback/testlab/-/testlab-8.0.4.tgz", @@ -2403,9 +2476,9 @@ } }, "node_modules/@sinonjs/fake-timers": { - "version": "13.0.5", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", - "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.2.0.tgz", + "integrity": "sha512-+SM3gQi95RWZLlD+Npy/UC5mHftlXwnVJMRpMyiqjrF4yNnbvi/Ubh3x9sLw6gxWSuibOn00uiLu1CKozehWlQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -2413,9 +2486,9 @@ } }, "node_modules/@sinonjs/samsam": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.3.tgz", - "integrity": "sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-9.0.3.tgz", + "integrity": "sha512-ZgYY7Dc2RW+OUdnZ1DEHg00lhRt+9BjymPKHog4PRFzr1U3MbK57+djmscWyKxzO1qfunHqs4N45WWyKIFKpiQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -2506,21 +2579,21 @@ } }, "node_modules/@types/express": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", - "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", "license": "MIT", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", - "@types/serve-static": "*" + "@types/serve-static": "^1" } }, "node_modules/@types/express-serve-static-core": { - "version": "4.19.6", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", - "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", "license": "MIT", "dependencies": { "@types/node": "*", @@ -3007,9 +3080,9 @@ } }, "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -3338,23 +3411,27 @@ "peer": true }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.0", + "debug": "^4.4.3", "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/bottleneck": { @@ -3923,9 +4000,9 @@ } }, "node_modules/commitizen/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -3972,9 +4049,9 @@ } }, "node_modules/commitizen/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -4222,9 +4299,9 @@ "license": "MIT" }, "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", "license": "MIT", "dependencies": { "object-assign": "^4", @@ -4232,6 +4309,10 @@ }, "engines": { "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/cosmiconfig": { @@ -5551,9 +5632,9 @@ } }, "node_modules/eslint/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -5568,9 +5649,9 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -5586,9 +5667,9 @@ "license": "MIT" }, "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -5723,39 +5804,39 @@ } }, "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.13.0", + "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", + "send": "~0.19.0", + "serve-static": "~1.16.2", "setprototypeof": "1.2.0", - "statuses": "2.0.1", + "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" @@ -5769,23 +5850,23 @@ } }, "node_modules/express/node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", + "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", "type-is": "~1.6.18", - "unpipe": "1.0.0" + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8", @@ -5801,6 +5882,26 @@ "ms": "2.0.0" } }, + "node_modules/express/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/express/node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -5829,18 +5930,18 @@ "license": "MIT" }, "node_modules/express/node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "license": "MIT" }, "node_modules/express/node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -5850,20 +5951,29 @@ } }, "node_modules/express/node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8" } }, + "node_modules/express/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/express/node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -6053,9 +6163,9 @@ } }, "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -6248,9 +6358,9 @@ } }, "node_modules/flat-cache/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -6281,9 +6391,9 @@ } }, "node_modules/flat-cache/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -6311,9 +6421,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -6433,9 +6543,9 @@ "license": "MIT" }, "node_modules/fs-extra": { - "version": "11.3.2", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", - "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", "dev": true, "license": "MIT", "dependencies": { @@ -6611,15 +6721,16 @@ } }, "node_modules/glob": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", - "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", - "minimatch": "^10.0.3", + "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" @@ -6647,17 +6758,40 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/glob/node_modules/minimatch": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", - "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "brace-expansion": "^5.0.5" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -6806,9 +6940,9 @@ "license": "MIT" }, "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", "license": "MIT", "dependencies": { "minimist": "^1.2.5", @@ -7097,15 +7231,19 @@ } }, "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/ieee754": { @@ -7597,9 +7735,9 @@ } }, "node_modules/istanbul-lib-processinfo/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -7630,9 +7768,9 @@ } }, "node_modules/istanbul-lib-processinfo/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -7730,13 +7868,13 @@ } }, "node_modules/jackspeak": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", - "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/cliui": "^8.0.2" + "@isaacs/cliui": "^9.0.0" }, "engines": { "node": "20 || >=22" @@ -7745,6 +7883,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/jackspeak/node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/jake": { "version": "10.9.4", "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", @@ -7780,9 +7928,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -8041,9 +8189,9 @@ "license": "MIT" }, "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", "dev": true, "license": "MIT" }, @@ -8194,64 +8342,122 @@ } }, "node_modules/loopback-connector": { - "version": "6.2.11", - "resolved": "https://registry.npmjs.org/loopback-connector/-/loopback-connector-6.2.11.tgz", - "integrity": "sha512-4jcFe64x7KNXTqp/vcBnl1M8wzPaFsL6RzVcCcZTUzGUEDdifi5Gc9VXu9Qbb0OcTNaE49KyeGG/LIXxuzV48A==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/loopback-connector/-/loopback-connector-7.0.4.tgz", + "integrity": "sha512-Xa0CkuwTYrDI3cB/e5CVJimvUhtNLLmW7XH5U+PME8GaOx0kvyog97ti0cHEXsRqztuDN84cpZqTSCxyhjBHKw==", "license": "MIT", "peer": true, "dependencies": { "async": "^3.2.6", "bluebird": "^3.7.2", - "debug": "^4.4.1", + "debug": "^4.4.3", "msgpack5": "^6.0.2", "strong-globalize": "^6.0.6", - "uuid": "^11.1.0" + "uuid": "^13.0.0" }, "engines": { - "node": ">=18" + "node": ">=20" + } + }, + "node_modules/loopback-connector/node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "peer": true, + "bin": { + "uuid": "dist-node/bin/uuid" } }, "node_modules/loopback-datasource-juggler": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/loopback-datasource-juggler/-/loopback-datasource-juggler-5.2.1.tgz", - "integrity": "sha512-AZr2i/bmlxJi9OM+9GdS0nPvbS6O/LNORqXE+IdQcjGDmMVKQZr2YLeNJiWU1kyHIHtQ7Q+LeNHyPAa8Usei8w==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/loopback-datasource-juggler/-/loopback-datasource-juggler-6.0.4.tgz", + "integrity": "sha512-SiyyOUgbEzbNGwY7iwAh9H5CITjbgZUJuuMHowfAbm1A9yb68DIy6Ynm2PQ5XkRov2+K9oD7hWry5FXcqoe81g==", "license": "MIT", "peer": true, "dependencies": { "async": "^3.2.6", "change-case": "^4.1.2", - "debug": "^4.4.1", + "debug": "^4.4.3", "depd": "^2.0.0", "inflection": "^3.0.2", - "lodash": "^4.17.21", - "loopback-connector": "^6.2.9", - "minimatch": "^10.0.3", + "lodash": "^4.17.23", + "loopback-connector": "^7.0.3", + "minimatch": "^10.2.4", "nanoid": "^3.3.11", "neotraverse": "^0.6.18", - "qs": "^6.14.0", + "qs": "^6.15.0", "strong-globalize": "^6.0.6", - "uuid": "^11.1.0" + "uuid": "^13.0.0" }, "engines": { - "node": ">=18" + "node": ">=20" + } + }, + "node_modules/loopback-datasource-juggler/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "peer": true, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/loopback-datasource-juggler/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, + "node_modules/loopback-datasource-juggler/node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT", + "peer": true + }, "node_modules/loopback-datasource-juggler/node_modules/minimatch": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", - "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", - "license": "ISC", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", "peer": true, "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "brace-expansion": "^5.0.5" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/loopback-datasource-juggler/node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "peer": true, + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/lower-case": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", @@ -8596,13 +8802,13 @@ } }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -8658,9 +8864,9 @@ } }, "node_modules/mocha": { - "version": "11.7.4", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.4.tgz", - "integrity": "sha512-1jYAaY8x0kAZ0XszLWu14pzsf4KV740Gld4HXkhNTXwcHx4AUEDkPzgEHg9CM5dVcW+zv036tjpsEbLraPJj4w==", + "version": "11.7.5", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", + "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", "dev": true, "license": "MIT", "dependencies": { @@ -8695,9 +8901,10 @@ } }, "node_modules/mocha/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -8992,9 +9199,9 @@ } }, "node_modules/npm": { - "version": "10.9.4", - "resolved": "https://registry.npmjs.org/npm/-/npm-10.9.4.tgz", - "integrity": "sha512-OnUG836FwboQIbqtefDNlyR0gTHzIfwRfE3DuiNewBvnMnWEpB0VEXwBlFVgqpNzIgYo/MHh3d2Hel/pszapAA==", + "version": "10.9.8", + "resolved": "https://registry.npmjs.org/npm/-/npm-10.9.8.tgz", + "integrity": "sha512-fYwb6ODSmHkqrJQQaCxY3M2lPf/mpgC7ik0HSzzIwG5CGtabRp4bNqikatvCoT42b5INQSqudVH0R7yVmC9hVg==", "bundleDependencies": [ "@isaacs/string-locale-compare", "@npmcli/arborist", @@ -9076,24 +9283,24 @@ ], "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/arborist": "^8.0.1", + "@npmcli/arborist": "^8.0.5", "@npmcli/config": "^9.0.0", "@npmcli/fs": "^4.0.0", "@npmcli/map-workspaces": "^4.0.2", "@npmcli/package-json": "^6.2.0", - "@npmcli/promise-spawn": "^8.0.2", + "@npmcli/promise-spawn": "^8.0.3", "@npmcli/redact": "^3.2.2", "@npmcli/run-script": "^9.1.0", "@sigstore/tuf": "^3.1.1", "abbrev": "^3.0.1", "archy": "~1.0.0", "cacache": "^19.0.1", - "chalk": "^5.4.1", - "ci-info": "^4.2.0", + "chalk": "^5.6.2", + "ci-info": "^4.4.0", "cli-columns": "^4.0.0", "fastest-levenshtein": "^1.0.16", "fs-minipass": "^3.0.3", - "glob": "^10.4.5", + "glob": "^10.5.0", "graceful-fs": "^4.2.11", "hosted-git-info": "^8.1.0", "ini": "^5.0.0", @@ -9101,46 +9308,46 @@ "is-cidr": "^5.1.1", "json-parse-even-better-errors": "^4.0.0", "libnpmaccess": "^9.0.0", - "libnpmdiff": "^7.0.1", - "libnpmexec": "^9.0.1", - "libnpmfund": "^6.0.1", + "libnpmdiff": "^7.0.5", + "libnpmexec": "^9.0.5", + "libnpmfund": "^6.0.5", "libnpmhook": "^11.0.0", "libnpmorg": "^7.0.0", - "libnpmpack": "^8.0.1", - "libnpmpublish": "^10.0.1", + "libnpmpack": "^8.0.5", + "libnpmpublish": "^10.0.2", "libnpmsearch": "^8.0.0", "libnpmteam": "^7.0.0", "libnpmversion": "^7.0.0", "make-fetch-happen": "^14.0.3", - "minimatch": "^9.0.5", - "minipass": "^7.1.1", + "minimatch": "^9.0.9", + "minipass": "^7.1.3", "minipass-pipeline": "^1.2.4", "ms": "^2.1.2", - "node-gyp": "^11.2.0", + "node-gyp": "^11.5.0", "nopt": "^8.1.0", - "normalize-package-data": "^7.0.0", + "normalize-package-data": "^7.0.1", "npm-audit-report": "^6.0.0", - "npm-install-checks": "^7.1.1", + "npm-install-checks": "^7.1.2", "npm-package-arg": "^12.0.2", "npm-pick-manifest": "^10.0.0", "npm-profile": "^11.0.1", "npm-registry-fetch": "^18.0.2", "npm-user-validate": "^3.0.0", - "p-map": "^7.0.3", + "p-map": "^7.0.4", "pacote": "^19.0.1", "parse-conflict-json": "^4.0.0", "proc-log": "^5.0.0", "qrcode-terminal": "^0.12.0", "read": "^4.1.0", - "semver": "^7.7.2", + "semver": "^7.7.4", "spdx-expression-parse": "^4.0.0", "ssri": "^12.0.0", "supports-color": "^9.4.0", - "tar": "^6.2.1", + "tar": "^7.5.11", "text-table": "~0.2.0", "tiny-relative-date": "^1.3.0", "treeverse": "^3.0.0", - "validate-npm-package-name": "^6.0.1", + "validate-npm-package-name": "^6.0.2", "which": "^5.0.0", "write-file-atomic": "^6.0.0" }, @@ -9182,7 +9389,7 @@ } }, "node_modules/npm/node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", + "version": "6.2.2", "dev": true, "inBundle": true, "license": "MIT", @@ -9217,12 +9424,12 @@ } }, "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", + "version": "7.2.0", "dev": true, "inBundle": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -9266,7 +9473,7 @@ } }, "node_modules/npm/node_modules/@npmcli/arborist": { - "version": "8.0.1", + "version": "8.0.5", "dev": true, "inBundle": true, "license": "ISC", @@ -9301,6 +9508,7 @@ "proggy": "^3.0.0", "promise-all-reject-late": "^1.0.0", "promise-call-limit": "^3.0.1", + "promise-retry": "^2.0.1", "read-package-json-fast": "^4.0.0", "semver": "^7.3.7", "ssri": "^12.0.0", @@ -9412,7 +9620,7 @@ } }, "node_modules/npm/node_modules/@npmcli/metavuln-calculator/node_modules/pacote": { - "version": "20.0.0", + "version": "20.0.1", "dev": true, "inBundle": true, "license": "ISC", @@ -9433,7 +9641,7 @@ "promise-retry": "^2.0.1", "sigstore": "^3.0.0", "ssri": "^12.0.0", - "tar": "^6.1.11" + "tar": "^7.5.10" }, "bin": { "pacote": "bin/index.js" @@ -9479,7 +9687,7 @@ } }, "node_modules/npm/node_modules/@npmcli/promise-spawn": { - "version": "8.0.2", + "version": "8.0.3", "dev": true, "inBundle": true, "license": "ISC", @@ -9538,6 +9746,27 @@ "node": ">=14" } }, + "node_modules/npm/node_modules/@sigstore/bundle": { + "version": "3.1.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.4.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@sigstore/core": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/npm/node_modules/@sigstore/protobuf-specs": { "version": "0.4.3", "dev": true, @@ -9547,6 +9776,23 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/npm/node_modules/@sigstore/sign": { + "version": "3.1.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^3.1.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.4.0", + "make-fetch-happen": "^14.0.2", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/npm/node_modules/@sigstore/tuf": { "version": "3.1.1", "dev": true, @@ -9560,6 +9806,20 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/npm/node_modules/@sigstore/verify": { + "version": "2.1.1", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^3.1.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.4.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/npm/node_modules/@tufjs/canonical-json": { "version": "2.0.0", "dev": true, @@ -9579,7 +9839,7 @@ } }, "node_modules/npm/node_modules/agent-base": { - "version": "7.1.3", + "version": "7.1.4", "dev": true, "inBundle": true, "license": "MIT", @@ -9597,7 +9857,7 @@ } }, "node_modules/npm/node_modules/ansi-styles": { - "version": "6.2.1", + "version": "6.2.3", "dev": true, "inBundle": true, "license": "MIT", @@ -9609,7 +9869,7 @@ } }, "node_modules/npm/node_modules/aproba": { - "version": "2.0.0", + "version": "2.1.0", "dev": true, "inBundle": true, "license": "ISC" @@ -9686,58 +9946,8 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/npm/node_modules/cacache/node_modules/chownr": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/npm/node_modules/cacache/node_modules/mkdirp": { - "version": "3.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/cacache/node_modules/tar": { - "version": "7.4.3", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/npm/node_modules/cacache/node_modules/yallist": { - "version": "5.0.0", - "dev": true, - "inBundle": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, "node_modules/npm/node_modules/chalk": { - "version": "5.4.1", + "version": "5.6.2", "dev": true, "inBundle": true, "license": "MIT", @@ -9749,16 +9959,16 @@ } }, "node_modules/npm/node_modules/chownr": { - "version": "2.0.0", + "version": "3.0.0", "dev": true, "inBundle": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/npm/node_modules/ci-info": { - "version": "4.2.0", + "version": "4.4.0", "dev": true, "funding": [ { @@ -9872,7 +10082,7 @@ } }, "node_modules/npm/node_modules/debug": { - "version": "4.4.1", + "version": "4.4.3", "dev": true, "inBundle": true, "license": "MIT", @@ -9889,7 +10099,7 @@ } }, "node_modules/npm/node_modules/diff": { - "version": "5.2.0", + "version": "5.2.2", "dev": true, "inBundle": true, "license": "BSD-3-Clause", @@ -9935,7 +10145,7 @@ "license": "MIT" }, "node_modules/npm/node_modules/exponential-backoff": { - "version": "3.1.2", + "version": "3.1.3", "dev": true, "inBundle": true, "license": "Apache-2.0" @@ -9949,6 +10159,23 @@ "node": ">= 4.9.1" } }, + "node_modules/npm/node_modules/fdir": { + "version": "6.5.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/npm/node_modules/foreground-child": { "version": "3.3.1", "dev": true, @@ -9978,7 +10205,7 @@ } }, "node_modules/npm/node_modules/glob": { - "version": "10.4.5", + "version": "10.5.0", "dev": true, "inBundle": true, "license": "ISC", @@ -10109,14 +10336,10 @@ } }, "node_modules/npm/node_modules/ip-address": { - "version": "9.0.5", + "version": "10.1.0", "dev": true, "inBundle": true, "license": "MIT", - "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - }, "engines": { "node": ">= 12" } @@ -10178,7 +10401,6 @@ "node_modules/npm/node_modules/jsbn": { "version": "1.1.0", "dev": true, - "inBundle": true, "license": "MIT" }, "node_modules/npm/node_modules/json-parse-even-better-errors": { @@ -10234,31 +10456,31 @@ } }, "node_modules/npm/node_modules/libnpmdiff": { - "version": "7.0.1", + "version": "7.0.5", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^8.0.1", + "@npmcli/arborist": "^8.0.5", "@npmcli/installed-package-contents": "^3.0.0", "binary-extensions": "^2.3.0", "diff": "^5.1.0", "minimatch": "^9.0.4", "npm-package-arg": "^12.0.0", "pacote": "^19.0.0", - "tar": "^6.2.1" + "tar": "^7.5.11" }, "engines": { "node": "^18.17.0 || >=20.5.0" } }, "node_modules/npm/node_modules/libnpmexec": { - "version": "9.0.1", + "version": "9.0.5", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^8.0.1", + "@npmcli/arborist": "^8.0.5", "@npmcli/run-script": "^9.0.1", "ci-info": "^4.0.0", "npm-package-arg": "^12.0.0", @@ -10274,12 +10496,12 @@ } }, "node_modules/npm/node_modules/libnpmfund": { - "version": "6.0.1", + "version": "6.0.5", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^8.0.1" + "@npmcli/arborist": "^8.0.5" }, "engines": { "node": "^18.17.0 || >=20.5.0" @@ -10312,12 +10534,12 @@ } }, "node_modules/npm/node_modules/libnpmpack": { - "version": "8.0.1", + "version": "8.0.5", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^8.0.1", + "@npmcli/arborist": "^8.0.5", "@npmcli/run-script": "^9.0.1", "npm-package-arg": "^12.0.0", "pacote": "^19.0.0" @@ -10327,7 +10549,7 @@ } }, "node_modules/npm/node_modules/libnpmpublish": { - "version": "10.0.1", + "version": "10.0.2", "dev": true, "inBundle": true, "license": "ISC", @@ -10414,22 +10636,13 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/npm/node_modules/make-fetch-happen/node_modules/negotiator": { - "version": "1.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/npm/node_modules/minimatch": { - "version": "9.0.5", + "version": "9.0.9", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -10439,10 +10652,10 @@ } }, "node_modules/npm/node_modules/minipass": { - "version": "7.1.2", + "version": "7.1.3", "dev": true, "inBundle": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -10500,6 +10713,12 @@ "node": ">=8" } }, + "node_modules/npm/node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, "node_modules/npm/node_modules/minipass-pipeline": { "version": "1.2.4", "dev": true, @@ -10524,6 +10743,12 @@ "node": ">=8" } }, + "node_modules/npm/node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, "node_modules/npm/node_modules/minipass-sized": { "version": "1.0.3", "dev": true, @@ -10548,8 +10773,14 @@ "node": ">=8" } }, + "node_modules/npm/node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, "node_modules/npm/node_modules/minizlib": { - "version": "3.0.2", + "version": "3.1.0", "dev": true, "inBundle": true, "license": "MIT", @@ -10560,18 +10791,6 @@ "node": ">= 18" } }, - "node_modules/npm/node_modules/mkdirp": { - "version": "1.0.4", - "dev": true, - "inBundle": true, - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/npm/node_modules/ms": { "version": "2.1.3", "dev": true, @@ -10587,8 +10806,17 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/npm/node_modules/negotiator": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/npm/node_modules/node-gyp": { - "version": "11.2.0", + "version": "11.5.0", "dev": true, "inBundle": true, "license": "MIT", @@ -10611,56 +10839,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/npm/node_modules/node-gyp/node_modules/chownr": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/mkdirp": { - "version": "3.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/tar": { - "version": "7.4.3", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/yallist": { - "version": "5.0.0", - "dev": true, - "inBundle": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, "node_modules/npm/node_modules/nopt": { "version": "8.1.0", "dev": true, @@ -10677,7 +10855,7 @@ } }, "node_modules/npm/node_modules/normalize-package-data": { - "version": "7.0.0", + "version": "7.0.1", "dev": true, "inBundle": true, "license": "BSD-2-Clause", @@ -10712,7 +10890,7 @@ } }, "node_modules/npm/node_modules/npm-install-checks": { - "version": "7.1.1", + "version": "7.1.2", "dev": true, "inBundle": true, "license": "BSD-2-Clause", @@ -10816,7 +10994,7 @@ } }, "node_modules/npm/node_modules/p-map": { - "version": "7.0.3", + "version": "7.0.4", "dev": true, "inBundle": true, "license": "MIT", @@ -10834,7 +11012,7 @@ "license": "BlueOak-1.0.0" }, "node_modules/npm/node_modules/pacote": { - "version": "19.0.1", + "version": "19.0.2", "dev": true, "inBundle": true, "license": "ISC", @@ -10855,7 +11033,7 @@ "promise-retry": "^2.0.1", "sigstore": "^3.0.0", "ssri": "^12.0.0", - "tar": "^6.1.11" + "tar": "^7.5.10" }, "bin": { "pacote": "bin/index.js" @@ -10903,8 +11081,20 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/npm/node_modules/picomatch": { + "version": "4.0.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/npm/node_modules/postcss-selector-parser": { - "version": "7.1.0", + "version": "7.1.1", "dev": true, "inBundle": true, "license": "MIT", @@ -11036,7 +11226,7 @@ "optional": true }, "node_modules/npm/node_modules/semver": { - "version": "7.7.2", + "version": "7.7.4", "dev": true, "inBundle": true, "license": "ISC", @@ -11097,58 +11287,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/npm/node_modules/sigstore/node_modules/@sigstore/bundle": { - "version": "3.1.0", - "dev": true, - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/protobuf-specs": "^0.4.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/sigstore/node_modules/@sigstore/core": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/sigstore/node_modules/@sigstore/sign": { - "version": "3.1.0", - "dev": true, - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/bundle": "^3.1.0", - "@sigstore/core": "^2.0.0", - "@sigstore/protobuf-specs": "^0.4.0", - "make-fetch-happen": "^14.0.2", - "proc-log": "^5.0.0", - "promise-retry": "^2.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/sigstore/node_modules/@sigstore/verify": { - "version": "2.1.1", - "dev": true, - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/bundle": "^3.1.0", - "@sigstore/core": "^2.0.0", - "@sigstore/protobuf-specs": "^0.4.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/npm/node_modules/smart-buffer": { "version": "4.2.0", "dev": true, @@ -11160,12 +11298,12 @@ } }, "node_modules/npm/node_modules/socks": { - "version": "2.8.5", + "version": "2.8.7", "dev": true, "inBundle": true, "license": "MIT", "dependencies": { - "ip-address": "^9.0.5", + "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" }, "engines": { @@ -11224,7 +11362,7 @@ } }, "node_modules/npm/node_modules/spdx-license-ids": { - "version": "3.0.21", + "version": "3.0.23", "dev": true, "inBundle": true, "license": "CC0-1.0" @@ -11232,7 +11370,6 @@ "node_modules/npm/node_modules/sprintf-js": { "version": "1.1.3", "dev": true, - "inBundle": true, "license": "BSD-3-Clause" }, "node_modules/npm/node_modules/ssri": { @@ -11314,78 +11451,19 @@ } }, "node_modules/npm/node_modules/tar": { - "version": "6.2.1", + "version": "7.5.11", "dev": true, "inBundle": true, - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/tar/node_modules/fs-minipass": { - "version": "2.1.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { - "version": "3.3.6", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/tar/node_modules/minipass": { - "version": "5.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/tar/node_modules/minizlib": { - "version": "2.1.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/tar/node_modules/minizlib/node_modules/minipass": { - "version": "3.3.6", - "dev": true, - "inBundle": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "yallist": "^4.0.0" + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" }, "engines": { - "node": ">=8" + "node": ">=18" } }, "node_modules/npm/node_modules/text-table": { @@ -11401,13 +11479,13 @@ "license": "MIT" }, "node_modules/npm/node_modules/tinyglobby": { - "version": "0.2.14", + "version": "0.2.15", "dev": true, "inBundle": true, "license": "MIT", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" @@ -11416,32 +11494,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/npm/node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.6", - "dev": true, - "inBundle": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/npm/node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/npm/node_modules/treeverse": { "version": "3.0.0", "dev": true, @@ -11452,14 +11504,14 @@ } }, "node_modules/npm/node_modules/tuf-js": { - "version": "3.0.1", + "version": "3.1.0", "dev": true, "inBundle": true, "license": "MIT", "dependencies": { "@tufjs/models": "3.0.1", - "debug": "^4.3.6", - "make-fetch-happen": "^14.0.1" + "debug": "^4.4.1", + "make-fetch-happen": "^14.0.3" }, "engines": { "node": "^18.17.0 || >=20.5.0" @@ -11529,7 +11581,7 @@ } }, "node_modules/npm/node_modules/validate-npm-package-name": { - "version": "6.0.1", + "version": "6.0.2", "dev": true, "inBundle": true, "license": "ISC", @@ -11559,12 +11611,12 @@ } }, "node_modules/npm/node_modules/which/node_modules/isexe": { - "version": "3.1.1", + "version": "3.1.5", "dev": true, "inBundle": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/npm/node_modules/wrap-ansi": { @@ -11618,7 +11670,7 @@ } }, "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.1.0", + "version": "6.2.2", "dev": true, "inBundle": true, "license": "MIT", @@ -11653,12 +11705,12 @@ } }, "node_modules/npm/node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.0", + "version": "7.2.0", "dev": true, "inBundle": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -11681,10 +11733,13 @@ } }, "node_modules/npm/node_modules/yallist": { - "version": "4.0.0", + "version": "5.0.0", "dev": true, "inBundle": true, - "license": "ISC" + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } }, "node_modules/nyc": { "version": "17.1.0", @@ -11729,9 +11784,9 @@ } }, "node_modules/nyc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -11801,9 +11856,9 @@ } }, "node_modules/nyc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -12611,9 +12666,9 @@ "license": "MIT" }, "node_modules/path-scurry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -12621,18 +12676,18 @@ "minipass": "^7.1.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", - "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } @@ -12660,9 +12715,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -12855,9 +12910,9 @@ } }, "node_modules/prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", "bin": { @@ -12927,9 +12982,9 @@ } }, "node_modules/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -12959,9 +13014,9 @@ } }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -13031,36 +13086,49 @@ } }, "node_modules/raw-body": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", - "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.7.0", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.10" } }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "node_modules/raw-body/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.8" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/express" } }, + "node_modules/raw-body/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -13562,9 +13630,10 @@ } }, "node_modules/rimraf/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -14614,9 +14683,9 @@ } }, "node_modules/simple-git": { - "version": "3.28.0", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.28.0.tgz", - "integrity": "sha512-Rs/vQRwsn1ILH1oBUy8NucJlXmnnLeLCfcvbSehkPzbv3wwoFWIdtfd6Ndo6ZPhlPsCZ60CPI4rxurnwAa+a2w==", + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.33.0.tgz", + "integrity": "sha512-D4V/tGC2sjsoNhoMybKyGoE+v8A60hRawKQ1iFRA1zwuDgGZCBJ4ByOzZ5J8joBbi4Oam0qiPH+GhzmSBwbJng==", "dev": true, "license": "MIT", "dependencies": { @@ -14630,16 +14699,16 @@ } }, "node_modules/sinon": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-21.0.0.tgz", - "integrity": "sha512-TOgRcwFPbfGtpqvZw+hyqJDvqfapr1qUlOizROIk4bBLjlsjlB00Pg6wMFXNtJRpu+eCZuVOaLatG7M8105kAw==", + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-21.0.3.tgz", + "integrity": "sha512-0x8TQFr8EjADhSME01u1ZK31yv2+bd6Z5NrBCHVM+n4qL1wFqbxftmeyi3bwlr49FbbzRfrqSFOpyHCOh/YmYA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.1", - "@sinonjs/fake-timers": "^13.0.5", - "@sinonjs/samsam": "^8.0.1", - "diff": "^7.0.0", + "@sinonjs/fake-timers": "^15.1.1", + "@sinonjs/samsam": "^9.0.3", + "diff": "^8.0.3", "supports-color": "^7.2.0" }, "funding": { @@ -14647,6 +14716,16 @@ "url": "https://opencollective.com/sinon" } }, + "node_modules/sinon/node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/skin-tone": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", @@ -14727,9 +14806,9 @@ } }, "node_modules/spawn-wrap/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -14774,9 +14853,9 @@ } }, "node_modules/spawn-wrap/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -15062,13 +15141,13 @@ } }, "node_modules/strong-error-handler": { - "version": "5.0.23", - "resolved": "https://registry.npmjs.org/strong-error-handler/-/strong-error-handler-5.0.23.tgz", - "integrity": "sha512-XU5wbYlQBuTaS48MnUDU1WO/Ih6Jc/5bFlMO81RbC3TcqHH1G4mQn6TpHhxupIsf8gW/kF9/9sEBLenYXle1mA==", + "version": "5.0.29", + "resolved": "https://registry.npmjs.org/strong-error-handler/-/strong-error-handler-5.0.29.tgz", + "integrity": "sha512-qeG7I2Dh62h3ZG1nfARQEZEKwRJzBurLipqRk6OPw8ScoynsGK54ui4v7rnry7c1nsfGWURzl9eAyDG7avlKRg==", "license": "MIT", "dependencies": { "accepts": "^1.3.8", - "debug": "^4.4.1", + "debug": "^4.4.3", "fast-safe-stringify": "^2.1.1", "handlebars": "^4.7.8", "http-status": "^1.8.1", @@ -15207,9 +15286,9 @@ } }, "node_modules/swagger-ui-dist": { - "version": "5.28.1", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.28.1.tgz", - "integrity": "sha512-IvPrtNi8MvjiuDgoSmPYgg27Lvu38fnLD1OSd8Y103xXsPAqezVNnNeHnVCZ/d+CMXJblflGaIyHxAYIF3O71w==", + "version": "5.31.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.31.0.tgz", + "integrity": "sha512-zSUTIck02fSga6rc0RZP3b7J7wgHXwLea8ZjgLA3Vgnb8QeOl3Wou2/j5QkzSGeoz6HusP/coYuJl33aQxQZpg==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "=1.4.0" @@ -15240,9 +15319,9 @@ } }, "node_modules/temp/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -15273,9 +15352,9 @@ } }, "node_modules/temp/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -15373,9 +15452,9 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -15406,9 +15485,9 @@ } }, "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -15552,9 +15631,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -15686,9 +15765,9 @@ } }, "node_modules/ts-node/node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -15761,15 +15840,19 @@ } }, "node_modules/type-is/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "license": "MIT", "dependencies": { "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/typedarray-to-buffer": { @@ -16029,9 +16112,9 @@ } }, "node_modules/validator": { - "version": "13.15.15", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", - "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", + "version": "13.15.26", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz", + "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==", "license": "MIT", "engines": { "node": ">= 0.10" @@ -16195,9 +16278,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", "license": "ISC", "engines": { "node": ">= 6" @@ -16227,9 +16310,9 @@ } }, "node_modules/yamljs/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -16240,7 +16323,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -16258,9 +16341,9 @@ } }, "node_modules/yamljs/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" diff --git a/src/providers/sdk/chargebee/adapter/subscription.adapter.ts b/src/providers/sdk/chargebee/adapter/subscription.adapter.ts index 04bf426..578ddc5 100644 --- a/src/providers/sdk/chargebee/adapter/subscription.adapter.ts +++ b/src/providers/sdk/chargebee/adapter/subscription.adapter.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import chargebee from 'chargebee'; import { IAdapter, TSubscriptionCreate, @@ -40,9 +41,10 @@ export interface RawChargebeeSubscription { * service.chargebeeSubscriptionAdapter = new MyAdapter(); * ``` */ -export class ChargebeeSubscriptionAdapter - implements IAdapter -{ +export class ChargebeeSubscriptionAdapter implements IAdapter< + TSubscriptionResult, + TSubscriptionCreate +> { /** * Maps a raw Chargebee Subscription object to the normalised * {@link TSubscriptionResult}. @@ -66,12 +68,11 @@ export class ChargebeeSubscriptionAdapter * * @param data - Provider-agnostic subscription creation payload. */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - adaptFromModel(data: Partial): any { + adaptFromModel( + data: TSubscriptionCreate, + ): Parameters[1] { return { - subscription_items: data.priceRefId - ? [{item_price_id: data.priceRefId}] - : [], + subscription_items: [{item_price_id: data.priceRefId}], discounts: [], ...(data.collectionMethod === 'send_invoice' ? { diff --git a/src/providers/sdk/chargebee/charge-bee.service.ts b/src/providers/sdk/chargebee/charge-bee.service.ts index 50916bf..55c06f5 100644 --- a/src/providers/sdk/chargebee/charge-bee.service.ts +++ b/src/providers/sdk/chargebee/charge-bee.service.ts @@ -38,10 +38,16 @@ export class ChargeBeeService implements IChargeBeeService { @inject(ChargeBeeBindings.config, {optional: true}) private readonly chargeBeeConfig: ChargeBeeConfig, ) { + if (!chargeBeeConfig) { + throw new Error( + 'ChargeBeeConfig binding is required. Provide a value for ChargeBeeBindings.config.', + ); + } + // Only configure the global chargebee singleton when a valid site is // provided. This prevents a second instantiation with empty config // (e.g. SDKProvider vs SubscriptionProvider) from resetting the site. - if (chargeBeeConfig?.site) { + if (chargeBeeConfig.site) { chargebee.configure({ site: chargeBeeConfig.site, api_key: chargeBeeConfig.apiKey, @@ -452,15 +458,28 @@ export class ChargeBeeService implements IChargeBeeService { updates: TSubscriptionUpdate, ): Promise { try { - const result = await chargebee.subscription - .update_for_items(subscriptionId, { - subscription_items: updates.priceRefId - ? [{item_price_id: updates.priceRefId}] - : [], - discounts: [], // Required by Chargebee SDK type + if (!updates.priceRefId) { + const existing = await chargebee.subscription + .retrieve(subscriptionId) + .request(); + return this.chargebeeSubscriptionAdapter.adaptToModel( + existing.subscription, + ); + } + + const updateParams: Parameters< + typeof chargebee.subscription.update_for_items + >[1] = { + subscription_items: [{item_price_id: updates.priceRefId}], + discounts: [], + ...(updates.prorationBehavior !== undefined && { // When prorationBehavior is 'none', pass prorate:false to suppress credit notes prorate: updates.prorationBehavior !== 'none', - }) + }), + }; + + const result = await chargebee.subscription + .update_for_items(subscriptionId, updateParams) .request(); return this.chargebeeSubscriptionAdapter.adaptToModel( result.subscription, diff --git a/src/providers/sdk/stripe/adapter/subscription.adapter.ts b/src/providers/sdk/stripe/adapter/subscription.adapter.ts index 5ed935c..479ed51 100644 --- a/src/providers/sdk/stripe/adapter/subscription.adapter.ts +++ b/src/providers/sdk/stripe/adapter/subscription.adapter.ts @@ -24,9 +24,10 @@ import { * } * ``` */ -export class StripeSubscriptionAdapter - implements IAdapter -{ +export class StripeSubscriptionAdapter implements IAdapter< + TSubscriptionResult, + TSubscriptionCreate +> { /** * Maps a raw Stripe Subscription object to the normalised * {@link TSubscriptionResult}. @@ -51,11 +52,10 @@ export class StripeSubscriptionAdapter * * @param data - Provider-agnostic subscription creation payload. */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - adaptFromModel(data: Partial): any { + adaptFromModel(data: TSubscriptionCreate): Stripe.SubscriptionCreateParams { return { customer: data.customerId, - items: data.priceRefId ? [{price: data.priceRefId}] : [], + items: [{price: data.priceRefId}], collection_method: data.collectionMethod, ...(data.daysUntilDue !== undefined && { days_until_due: data.daysUntilDue, diff --git a/src/providers/sdk/stripe/stripe.service.ts b/src/providers/sdk/stripe/stripe.service.ts index 90d3429..a24e6c7 100644 --- a/src/providers/sdk/stripe/stripe.service.ts +++ b/src/providers/sdk/stripe/stripe.service.ts @@ -388,7 +388,14 @@ export class StripeService implements IStripeService { }; } - const priceItemId = existing.items.data[0].id; + const items = existing.items?.data; + if (!items || items.length === 0) { + throw new Error( + `Subscription ${subscriptionId} has no items and cannot be updated`, + ); + } + + const priceItemId = items[0].id; const updated = await this.stripe.subscriptions.update(subscriptionId, { proration_behavior: updates.prorationBehavior as Stripe.SubscriptionUpdateParams.ProrationBehavior, @@ -429,12 +436,12 @@ export class StripeService implements IStripeService { ); } catch (err) { // Invoice cleanup is best-effort after cancellation. - // Surface as a structured error so callers and APM tools can observe it. - throw Object.assign( - new Error( - `[StripeService] cancelSubscription: invoice cleanup failed for ${subscriptionId}`, - ), - {cause: err}, + // The subscription is already cancelled at this point, so do not + // propagate cleanup failures as cancel failures. + process.emitWarning( + `[StripeService] cancelSubscription: invoice cleanup failed for ${subscriptionId}: ${String( + err, + )}`, ); } } diff --git a/src/providers/sdk/stripe/type/stripe-config.type.ts b/src/providers/sdk/stripe/type/stripe-config.type.ts index 3685c4e..2cf95ea 100644 --- a/src/providers/sdk/stripe/type/stripe-config.type.ts +++ b/src/providers/sdk/stripe/type/stripe-config.type.ts @@ -1,3 +1,4 @@ +import Stripe from 'stripe'; export interface StripeConfig { secretKey: string; /** @@ -10,5 +11,5 @@ export interface StripeConfig { * * @see https://stripe.com/docs/api/subscriptions/create#create_subscription-payment_behavior */ - defaultPaymentBehavior?: string; + defaultPaymentBehavior?: Stripe.SubscriptionCreateParams.PaymentBehavior; }