Skip to content

Commit 0f07cd5

Browse files
authored
Integrate Membership API functionality (#76)
* support creating checkout session * support provisioning a member in SQS * change actor name for provisioning * clean up logging * build route * fix stripe endpoint * use event ID as the initiator
1 parent 93dca44 commit 0f07cd5

File tree

10 files changed

+482
-97
lines changed

10 files changed

+482
-97
lines changed

.eslintrc

+19-5
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
"plugin:prettier/recommended",
44
"plugin:@typescript-eslint/recommended"
55
],
6-
"plugins": ["import"],
6+
"plugins": [
7+
"import",
8+
"prettier"
9+
],
710
"rules": {
811
"import/no-unresolved": "error",
912
"import/extensions": [
@@ -32,27 +35,38 @@
3235
},
3336
"settings": {
3437
"import/parsers": {
35-
"@typescript-eslint/parser": [".ts", ".tsx", ".js", ".jsx"]
38+
"@typescript-eslint/parser": [
39+
".ts",
40+
".tsx",
41+
".js",
42+
".jsx"
43+
]
3644
},
3745
"import/resolver": {
3846
"typescript": {
3947
"alwaysTryTypes": true,
4048
"project": [
4149
"src/api/tsconfig.json", // Path to tsconfig.json in src/api
42-
"src/ui/tsconfig.json" // Path to tsconfig.json in src/ui
50+
"src/ui/tsconfig.json" // Path to tsconfig.json in src/ui
4351
]
4452
}
4553
}
4654
},
4755
"overrides": [
4856
{
49-
"files": ["*.test.ts", "*.testdata.ts"],
57+
"files": [
58+
"*.test.ts",
59+
"*.testdata.ts"
60+
],
5061
"rules": {
5162
"@typescript-eslint/no-explicit-any": "off"
5263
}
5364
},
5465
{
55-
"files": ["src/ui/*", "src/ui/**/*"],
66+
"files": [
67+
"src/ui/*",
68+
"src/ui/**/*"
69+
],
5670
"rules": {
5771
"@typescript-eslint/no-explicit-any": "off",
5872
"@typescript-eslint/no-unused-vars": "off"

src/api/functions/membership.ts

+62-8
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import {
2+
ConditionalCheckFailedException,
23
DynamoDBClient,
34
PutItemCommand,
45
QueryCommand,
56
} from "@aws-sdk/client-dynamodb";
67
import { marshall } from "@aws-sdk/util-dynamodb";
78
import { genericConfig } from "common/config.js";
89
import { FastifyBaseLogger } from "fastify";
9-
import { isUserInGroup } from "./entraId.js";
10+
import { isUserInGroup, modifyGroup } from "./entraId.js";
1011
import { EntraGroupError } from "common/errors/index.js";
12+
import { EntraGroupActions } from "common/types/iam.js";
1113

1214
export async function checkPaidMembership(
1315
endpoint: string,
@@ -76,17 +78,69 @@ export async function checkPaidMembershipFromEntra(
7678
export async function setPaidMembershipInTable(
7779
netId: string,
7880
dynamoClient: DynamoDBClient,
79-
): Promise<void> {
81+
actor: string = "core-api-queried",
82+
): Promise<{ updated: boolean }> {
8083
const obj = {
8184
email: `${netId}@illinois.edu`,
8285
inserted_at: new Date().toISOString(),
83-
inserted_by: "membership-api-queried",
86+
inserted_by: actor,
8487
};
8588

86-
await dynamoClient.send(
87-
new PutItemCommand({
88-
TableName: genericConfig.MembershipTableName,
89-
Item: marshall(obj),
90-
}),
89+
try {
90+
await dynamoClient.send(
91+
new PutItemCommand({
92+
TableName: genericConfig.MembershipTableName,
93+
Item: marshall(obj),
94+
ConditionExpression: "attribute_not_exists(email)",
95+
}),
96+
);
97+
return { updated: true };
98+
} catch (error: unknown) {
99+
if (error instanceof ConditionalCheckFailedException) {
100+
return { updated: false };
101+
}
102+
throw error;
103+
}
104+
}
105+
106+
type SetPaidMembershipInput = {
107+
netId: string;
108+
dynamoClient: DynamoDBClient;
109+
entraToken: string;
110+
paidMemberGroup: string;
111+
};
112+
113+
type SetPaidMembershipOutput = {
114+
updated: boolean;
115+
};
116+
117+
export async function setPaidMembership({
118+
netId,
119+
dynamoClient,
120+
entraToken,
121+
paidMemberGroup,
122+
}: SetPaidMembershipInput): Promise<SetPaidMembershipOutput> {
123+
const dynamoResult = await setPaidMembershipInTable(
124+
netId,
125+
dynamoClient,
126+
"core-api-provisioned",
91127
);
128+
if (!dynamoResult.updated) {
129+
const inEntra = await checkPaidMembershipFromEntra(
130+
netId,
131+
entraToken,
132+
paidMemberGroup,
133+
);
134+
if (inEntra) {
135+
return { updated: false };
136+
}
137+
}
138+
await modifyGroup(
139+
entraToken,
140+
`${netId}@illinois.edu`,
141+
paidMemberGroup,
142+
EntraGroupActions.ADD,
143+
);
144+
145+
return { updated: true };
92146
}

src/api/functions/stripe.ts

+42
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { InternalServerError } from "common/errors/index.js";
12
import Stripe from "stripe";
23

34
export type StripeLinkCreateParams = {
@@ -9,6 +10,15 @@ export type StripeLinkCreateParams = {
910
stripeApiKey: string;
1011
};
1112

13+
export type StripeCheckoutSessionCreateParams = {
14+
successUrl?: string;
15+
returnUrl?: string;
16+
customerEmail?: string;
17+
stripeApiKey: string;
18+
items: { price: string; quantity: number }[];
19+
initiator: string;
20+
};
21+
1222
/**
1323
* Create a Stripe payment link for an invoice. Note that invoiceAmountUsd MUST IN CENTS!!
1424
* @param {StripeLinkCreateParams} options
@@ -53,3 +63,35 @@ export const createStripeLink = async ({
5363
priceId: price.id,
5464
};
5565
};
66+
67+
export const createCheckoutSession = async ({
68+
successUrl,
69+
returnUrl,
70+
stripeApiKey,
71+
customerEmail,
72+
items,
73+
initiator,
74+
}: StripeCheckoutSessionCreateParams): Promise<string> => {
75+
const stripe = new Stripe(stripeApiKey);
76+
const payload: Stripe.Checkout.SessionCreateParams = {
77+
success_url: successUrl || "",
78+
cancel_url: returnUrl || "",
79+
payment_method_types: ["card"],
80+
line_items: items.map((item) => ({
81+
price: item.price,
82+
quantity: item.quantity,
83+
})),
84+
mode: "payment",
85+
customer_email: customerEmail,
86+
metadata: {
87+
initiator,
88+
},
89+
};
90+
const session = await stripe.checkout.sessions.create(payload);
91+
if (!session.url) {
92+
throw new InternalServerError({
93+
message: "Could not create Stripe checkout session.",
94+
});
95+
}
96+
return session.url;
97+
};

src/api/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"esbuild": "^0.24.2",
3737
"fastify": "^5.1.0",
3838
"fastify-plugin": "^4.5.1",
39+
"fastify-raw-body": "^5.0.0",
3940
"ical-generator": "^7.2.0",
4041
"jsonwebtoken": "^9.0.2",
4142
"jwks-rsa": "^3.1.0",

0 commit comments

Comments
 (0)