Skip to content

Commit

Permalink
feat: Configure free trial without payment details [DEV-4455] (#613)
Browse files Browse the repository at this point in the history
* feat: Configure free trial without payment details

* fix: Treat 404 response status on Logto role removal as success

Signed-off-by: jay-dee7 <me@jsdp.dev>

---------

Signed-off-by: jay-dee7 <me@jsdp.dev>
Co-authored-by: jay-dee7 <me@jsdp.dev>
  • Loading branch information
DaevMithran and jay-dee7 authored Oct 25, 2024
1 parent 52701eb commit f018051
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 63 deletions.
34 changes: 17 additions & 17 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
111 changes: 73 additions & 38 deletions src/controllers/admin/subscriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -180,54 +181,88 @@ 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}`,
} satisfies SubscriptionCreateUnsuccessfulResponseBody);
}
}

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<Response> {
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
*
Expand Down
6 changes: 2 additions & 4 deletions src/controllers/api/accreditation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 3 additions & 1 deletion src/middleware/auth/logto-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({});
}

Expand Down
3 changes: 2 additions & 1 deletion src/types/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
1 change: 1 addition & 0 deletions src/types/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

0 comments on commit f018051

Please sign in to comment.