diff --git a/src/controllers/admin/subscriptions.ts b/src/controllers/admin/subscriptions.ts index 47422d2a..442dd372 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,53 +181,19 @@ export class SubscriptionController { @validate async create(request: Request, response: Response) { - const stripe = response.locals.stripe as Stripe; + const body: SubscriptionCreateRequestBody = request.body; - 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_settings: { - end_behavior: { - missing_payment_method: 'cancel', - }, - }, - trial_period_days: trialPeriodDays, - }, - payment_method_collection: 'if_required', - }, - { - idempotencyKey, - } - ); - - 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}`, @@ -234,6 +201,70 @@ 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.OK).json({ + sessionURL: '' as string, + } satisfies SubscriptionCreateResponseBody); + } + /** * @openapi * diff --git a/src/services/identity/agent.ts b/src/services/identity/agent.ts index 757439a7..2b11e273 100644 --- a/src/services/identity/agent.ts +++ b/src/services/identity/agent.ts @@ -55,7 +55,7 @@ import { DefaultStatusList2021StatusPurposeType, TransactionResult, } from '@cheqd/did-provider-cheqd'; -import type { CheqdNetwork } from '@cheqd/sdk'; +import { ResourceModule, type CheqdNetwork } from '@cheqd/sdk'; import { getDidKeyResolver as KeyDidResolver } from '@veramo/did-provider-key'; import { Resolver, ResolverRegistry } from 'did-resolver'; import { DefaultDidUrlPattern, CreateAgentRequest, VeramoAgent } from '../../types/shared.js'; @@ -305,6 +305,10 @@ export class Veramo { payload, network: network as CheqdNetwork, signInputs: publicKeyHexs, + fee: { + amount: [ResourceModule.fees.DefaultCreateResourceJsonFee], + gas: Number(2_050_000).toString(), + }, } satisfies ICheqdCreateLinkedResourceArgs); return result; } catch (error) { diff --git a/src/types/admin.ts b/src/types/admin.ts index a3266fd0..057b1ebb 100644 --- a/src/types/admin.ts +++ b/src/types/admin.ts @@ -37,6 +37,7 @@ export type SubscriptionCreateRequestBody = { quantity?: number; trialPeriodDays?: number; idempotencyKey?: string; + trialMode?: boolean; }; export type SubscriptionCreateResponseBody = { 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;