From f01805175479d111952a2e9c047880a873b2993e Mon Sep 17 00:00:00 2001 From: DaevMithran <61043607+DaevMithran@users.noreply.github.com> Date: Fri, 25 Oct 2024 11:26:48 +0400 Subject: [PATCH] feat: Configure free trial without payment details [DEV-4455] (#613) * feat: Configure free trial without payment details * fix: Treat 404 response status on Logto role removal as success Signed-off-by: jay-dee7 --------- Signed-off-by: jay-dee7 Co-authored-by: jay-dee7 --- package-lock.json | 34 ++++---- package.json | 4 +- src/controllers/admin/subscriptions.ts | 111 ++++++++++++++++--------- src/controllers/api/accreditation.ts | 6 +- src/middleware/auth/logto-helper.ts | 4 +- src/types/admin.ts | 3 +- src/types/constants.ts | 1 + 7 files changed, 100 insertions(+), 63 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5dedbfd5..95a5b797 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "@cheqd/studio", - "version": "3.4.0-develop.3", + "version": "3.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@cheqd/studio", - "version": "3.4.0-develop.3", + "version": "3.4.0", "license": "Apache-2.0", "dependencies": { - "@cheqd/did-provider-cheqd": "^4.2.0", - "@cheqd/sdk": "^4.0.4", + "@cheqd/did-provider-cheqd": "^4.2.1-develop.1", + "@cheqd/sdk": "^4.0.5", "@cheqd/ts-proto": "^3.4.4", "@cosmjs/amino": "^0.32.4", "@cosmjs/encoding": "^0.32.4", @@ -2822,12 +2822,12 @@ } }, "node_modules/@cheqd/did-provider-cheqd": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@cheqd/did-provider-cheqd/-/did-provider-cheqd-4.2.0.tgz", - "integrity": "sha512-lrtj2txzNCaPZS0syV0NT4Igj+1eGc/ITZk7o08QOXBlUPwrULMTsBhiBJEbfUv+vcSGe2QdZaD5m6v/jRaiUw==", + "version": "4.2.1-develop.1", + "resolved": "https://registry.npmjs.org/@cheqd/did-provider-cheqd/-/did-provider-cheqd-4.2.1-develop.1.tgz", + "integrity": "sha512-pk0h4fYynBYUvbUBbNhIgxMax+v8teN+QWHdV0pA2+UZObHw1bQMH8RG+321uEa6PHaRWpNmt0hFxBoFzSbj2Q==", "license": "Apache-2.0", "dependencies": { - "@cheqd/sdk": "^4.0.3", + "@cheqd/sdk": "^4.0.5", "@cheqd/ts-proto": "^3.4.4", "@cosmjs/amino": "^0.32.4", "@cosmjs/crypto": "^0.32.4", @@ -2835,16 +2835,16 @@ "@cosmjs/stargate": "^0.32.4", "@cosmjs/utils": "^0.32.4", "@digitalbazaar/vc-status-list": "^8.0.0", - "@lit-protocol/encryption-v2": "npm:@lit-protocol/encryption@2.2.63", - "@lit-protocol/lit-node-client": "^6.4.1", - "@lit-protocol/lit-node-client-v2": "npm:@lit-protocol/lit-node-client@2.2.63", - "@lit-protocol/lit-node-client-v3": "npm:@lit-protocol/lit-node-client@3.1.1", + "@lit-protocol/encryption-v2": "npm:@lit-protocol/encryption@~2.2.63", + "@lit-protocol/lit-node-client": "^6.4.10", + "@lit-protocol/lit-node-client-v2": "npm:@lit-protocol/lit-node-client@~2.2.63", + "@lit-protocol/lit-node-client-v3": "npm:@lit-protocol/lit-node-client@~3.1.1", "@veramo/core": "^6.0.0", "@veramo/did-manager": "^6.0.0", "@veramo/did-provider-key": "^6.0.0", "@veramo/key-manager": "^6.0.0", "@veramo/utils": "^6.0.0", - "debug": "^4.3.6", + "debug": "^4.3.7", "did-jwt": "^8.0.4", "did-resolver": "^4.1.0", "generate-password": "^1.7.1", @@ -2856,9 +2856,9 @@ } }, "node_modules/@cheqd/sdk": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@cheqd/sdk/-/sdk-4.0.4.tgz", - "integrity": "sha512-Jb3cwSeF9fftgnlKSporpDAFCY5jCghaUysFDQEOqAutJHT+UfF+INkQ+sibWPbp8A/x7HsSDrXddfQSpouLcQ==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@cheqd/sdk/-/sdk-4.0.5.tgz", + "integrity": "sha512-M2PF+fJPfdLj+w4i7D+2hf+hXC1C+p0szchN1lYkQKfFdA17IT459QZ0IqHWbKMRgvEuy392zmh4d0hJVrOwlw==", "license": "Apache-2.0", "dependencies": { "@cheqd/ts-proto": "^3.4.4", @@ -2876,7 +2876,7 @@ "did-jwt": "^8.0.4", "did-resolver": "^4.1.0", "file-type": "^19.5.0", - "multiformats": "^13.2.2", + "multiformats": "^13.3.0", "secp256k1": "^5.0.0", "uuid": "^10.0.0" }, diff --git a/package.json b/package.json index 30a4c98c..929bd9fd 100644 --- a/package.json +++ b/package.json @@ -53,8 +53,8 @@ "README.md" ], "dependencies": { - "@cheqd/did-provider-cheqd": "^4.2.0", - "@cheqd/sdk": "^4.0.4", + "@cheqd/did-provider-cheqd": "^4.2.1-develop.1", + "@cheqd/sdk": "^4.0.5", "@cheqd/ts-proto": "^3.4.4", "@cosmjs/amino": "^0.32.4", "@cosmjs/encoding": "^0.32.4", diff --git a/src/controllers/admin/subscriptions.ts b/src/controllers/admin/subscriptions.ts index e91cd6ea..560182b7 100644 --- a/src/controllers/admin/subscriptions.ts +++ b/src/controllers/admin/subscriptions.ts @@ -27,6 +27,7 @@ import { SubscriptionService } from '../../services/admin/subscription.js'; import { stripeService } from '../../services/admin/stripe.js'; import type { UnsuccessfulResponseBody } from '../../types/shared.js'; import { validate } from '../validator/decorator.js'; +import { MaxAllowedTrialPeriodDays } from '../../types/constants.js'; dotenv.config(); @@ -180,47 +181,19 @@ export class SubscriptionController { @validate async create(request: Request, response: Response) { - const stripe = response.locals.stripe as Stripe; - - const { price, successURL, cancelURL, quantity, idempotencyKey, trialPeriodDays } = - request.body satisfies SubscriptionCreateRequestBody; - try { - const session = await stripe.checkout.sessions.create( - { - mode: 'subscription', - customer: response.locals.customer.paymentProviderId, - line_items: [ - { - price: price, - quantity: quantity || 1, - }, - ], - success_url: successURL, - cancel_url: cancelURL, - subscription_data: { - trial_period_days: trialPeriodDays, - }, - }, - { - idempotencyKey, - } - ); + const body: SubscriptionCreateRequestBody = request.body; - if (session.lastResponse?.statusCode !== StatusCodes.OK) { - return response.status(StatusCodes.BAD_GATEWAY).json({ - error: 'Checkout session was not created', - } satisfies SubscriptionCreateUnsuccessfulResponseBody); - } + // ensure trials don't succeed 30 days (default) + if (!body.trialPeriodDays || body.trialPeriodDays > MaxAllowedTrialPeriodDays) { + body.trialPeriodDays = MaxAllowedTrialPeriodDays; + } - if (!session.url) { - return response.status(StatusCodes.BAD_GATEWAY).json({ - error: 'Checkout session URL was not provided', - } satisfies SubscriptionCreateUnsuccessfulResponseBody); + try { + if (body.trialMode) { + return await this.startTrialForPlan(response, body); + } else { + return await this.createCheckoutSession(response, body); } - - return response.json({ - sessionURL: session.url as string, - } satisfies SubscriptionCreateResponseBody); } catch (error) { return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ error: `Internal error: ${(error as Error)?.message || error}`, @@ -228,6 +201,68 @@ export class SubscriptionController { } } + async createCheckoutSession(response: Response, body: SubscriptionCreateRequestBody) { + const stripe = response.locals.stripe as Stripe; + const { price, quantity = 1, cancelURL, successURL, idempotencyKey, trialPeriodDays } = body; + + const session = await stripe.checkout.sessions.create( + { + mode: 'subscription', + customer: response.locals.customer.paymentProviderId, + line_items: [{ price, quantity }], + success_url: successURL, + cancel_url: cancelURL, + subscription_data: { + trial_period_days: trialPeriodDays, + }, + }, + { idempotencyKey } + ); + + if (session.lastResponse?.statusCode !== StatusCodes.OK) { + return response.status(StatusCodes.BAD_GATEWAY).json({ + error: 'Checkout session was not created', + } satisfies SubscriptionCreateUnsuccessfulResponseBody); + } + + if (!session.url) { + return response.status(StatusCodes.BAD_GATEWAY).json({ + error: 'Checkout session URL was not provided', + } satisfies SubscriptionCreateUnsuccessfulResponseBody); + } + + return response.json({ + sessionURL: session.url as string, + } satisfies SubscriptionCreateResponseBody); + } + + async startTrialForPlan(response: Response, body: SubscriptionCreateRequestBody): Promise { + const stripe = response.locals.stripe as Stripe; + + const { price, quantity = 1, trialPeriodDays } = body; + const subscriptionResponse = await stripe.subscriptions.create({ + customer: response.locals.customer.paymentProviderId, + items: [{ price: price, quantity: quantity }], + payment_settings: { + save_default_payment_method: 'on_subscription', + }, + trial_period_days: trialPeriodDays, + trial_settings: { + end_behavior: { + missing_payment_method: 'cancel', + }, + }, + }); + + if (subscriptionResponse.lastResponse.statusCode !== StatusCodes.OK) { + return response.status(StatusCodes.BAD_GATEWAY).json({ + error: 'Failed to start trial plan', + } satisfies SubscriptionCreateUnsuccessfulResponseBody); + } + + return response.status(StatusCodes.NO_CONTENT); + } + /** * @openapi * diff --git a/src/controllers/api/accreditation.ts b/src/controllers/api/accreditation.ts index 442fad62..7e50acd2 100644 --- a/src/controllers/api/accreditation.ts +++ b/src/controllers/api/accreditation.ts @@ -502,11 +502,9 @@ export class AccreditationController { const resource = await res.json(); if (resource.dereferencingMetadata) { - return { - success: false, - status: 404, + return response.status(StatusCodes.NOT_FOUND).json({ error: `DID URL ${didUrl} is not found`, - }; + }); } const accreditation: CheqdW3CVerifiableCredential = resource; diff --git a/src/middleware/auth/logto-helper.ts b/src/middleware/auth/logto-helper.ts index 8138f4ea..e3d5a741 100644 --- a/src/middleware/auth/logto-helper.ts +++ b/src/middleware/auth/logto-helper.ts @@ -345,7 +345,9 @@ export class LogToHelper extends OAuthProvider implements IOAuthProvider { Authorization: 'Bearer ' + (await this.getM2MToken()), }, }); - if (response.status === StatusCodes.NO_CONTENT) { + + // check if role is removed or doesn't to begin with + if (response.status === StatusCodes.NO_CONTENT || response.status === StatusCodes.NOT_FOUND) { return this.returnOk({}); } diff --git a/src/types/admin.ts b/src/types/admin.ts index a3266fd0..e8b5c25b 100644 --- a/src/types/admin.ts +++ b/src/types/admin.ts @@ -37,10 +37,11 @@ export type SubscriptionCreateRequestBody = { quantity?: number; trialPeriodDays?: number; idempotencyKey?: string; + trialMode?: boolean; }; export type SubscriptionCreateResponseBody = { - sessionURL: Stripe.Checkout.Session['client_secret']; + sessionURL?: Stripe.Checkout.Session['url']; }; export type SubscriptionUpdateResponseBody = { diff --git a/src/types/constants.ts b/src/types/constants.ts index 0aebd3af..64108d6b 100644 --- a/src/types/constants.ts +++ b/src/types/constants.ts @@ -151,3 +151,4 @@ export const StatusList2021Entry = 'StatusList2021Entry'; export const JSONLD_PROOF_TYPES = ['Ed25519Signature2018', 'Ed25519Signature2020', 'JsonWebSignature2020']; export const DEFAULT_PAGINATION_LIST_LIMIT = 10; export const DefaultStudioRoleName = 'default' as const; +export const MaxAllowedTrialPeriodDays = 30;