Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
116 commits
Select commit Hold shift + click to select a range
d0e7d9f
rough version of checkout session upon link usage
AdityaK2905 Feb 10, 2026
444dbe0
reverted rsvp changes
AdityaK2905 Feb 10, 2026
1361491
coderabbit fixes plus writing payments
AdityaK2905 Feb 10, 2026
d8d0d97
wrapped payment recording in try catch
AdityaK2905 Feb 10, 2026
a3f4428
rebasing off main and changed checkout session to use ACH
AdityaK2905 Feb 10, 2026
8f0b844
updated zod schema
AdityaK2905 Feb 10, 2026
060651e
reset yarn.lock
AdityaK2905 Feb 10, 2026
43e6877
Add ACM org to UI
devksingh4 Feb 10, 2026
256d035
changed dollars to cents in zod
AdityaK2905 Feb 10, 2026
f9bb03f
updated tests
AdityaK2905 Feb 17, 2026
9f6432d
readded stripe types
AdityaK2905 Feb 17, 2026
a7003d5
fixed tests with db commands changing
AdityaK2905 Feb 17, 2026
231fe3d
acmOrg field for createLink test
AdityaK2905 Feb 17, 2026
bc059ef
improved test coverage
AdityaK2905 Feb 17, 2026
3de0b50
changed vi mock pls pass tests
AdityaK2905 Feb 17, 2026
d1006dd
please pass
AdityaK2905 Feb 17, 2026
c8b1f4f
fixed tests
AdityaK2905 Feb 17, 2026
fd304df
changed return status order
AdityaK2905 Feb 17, 2026
f38ddea
updated live tests
AdityaK2905 Feb 17, 2026
1648781
updated for codeQL
AdityaK2905 Feb 17, 2026
399e333
empty commit for live tests
AdityaK2905 Feb 17, 2026
c58ffcf
Don't return contactName and contactEmail in payment links GET
devksingh4 Feb 17, 2026
ea2e99c
Payment entry upon checkout session payment enters dynamoDB locally now
AdityaK2905 Feb 24, 2026
1b5f8d6
fixed unused imports and other errors
AdityaK2905 Feb 24, 2026
b5c7e1e
updated UI, only pay upon sufficient funding
AdityaK2905 Mar 3, 2026
ee66076
updated to pass tests
AdityaK2905 Mar 3, 2026
8878d6b
updated createlink test
AdityaK2905 Mar 3, 2026
49a8b1b
updated placeholder UI text tests
AdityaK2905 Mar 3, 2026
377b814
added lead role for live stripe testing
AdityaK2905 Mar 3, 2026
3ae9fae
logs
AdityaK2905 Mar 3, 2026
7376955
updated variables to give payment links table access
AdityaK2905 Mar 4, 2026
14309cf
fixed wrong variables
AdityaK2905 Mar 4, 2026
fba9fde
added stripe payment links table to dynamo access
AdityaK2905 Mar 4, 2026
611a35d
updated live stripe deletion test
AdityaK2905 Mar 4, 2026
ad78e02
removed needsconfirmation and changed live tests
AdityaK2905 Mar 4, 2026
86db27e
removed confirmation test
AdityaK2905 Mar 4, 2026
5ee9152
skipping tests
AdityaK2905 Mar 4, 2026
d3461bf
rebased and fixed files
AdityaK2905 Mar 7, 2026
b15d390
fixed rsvp types
AdityaK2905 Mar 7, 2026
5138776
Resetting yarn
AdityaK2905 Mar 9, 2026
ae607f8
Auto-update feature branch with changes from the main branch
github-actions[bot] Mar 9, 2026
553ec4e
emailing hopefully
AdityaK2905 Mar 10, 2026
b1cf1e7
updated stripe POST webhook test
AdityaK2905 Mar 10, 2026
823ce82
Auto-update feature branch with changes from the main branch
github-actions[bot] Mar 12, 2026
116aa62
Auto-update feature branch with changes from the main branch
github-actions[bot] Mar 12, 2026
2adeeb2
Auto-update feature branch with changes from the main branch
github-actions[bot] Mar 18, 2026
02f2f39
Auto-update feature branch with changes from the main branch
github-actions[bot] Mar 18, 2026
a997062
Auto-update feature branch with changes from the main branch
github-actions[bot] Mar 18, 2026
39c6ef5
Auto-update feature branch with changes from the main branch
github-actions[bot] Mar 18, 2026
0686ee8
Auto-update feature branch with changes from the main branch
github-actions[bot] Mar 20, 2026
1470806
fixed emails, immediate success payment, and stripe links table
AdityaK2905 Mar 24, 2026
bd7bcb0
fixed for tests
AdityaK2905 Mar 24, 2026
d06c11f
updated unit tests
AdityaK2905 Mar 24, 2026
d6cdc56
forgot to update types
AdityaK2905 Mar 24, 2026
3a7f4f7
updating return url
AdityaK2905 Mar 24, 2026
0589cb2
updated emails and success urls again
AdityaK2905 Mar 24, 2026
33ada1d
removing doubled emails
AdityaK2905 Mar 24, 2026
a57a236
fixed emails to deploy
AdityaK2905 Mar 24, 2026
98e6ccd
changing success url
AdityaK2905 Mar 24, 2026
dac93d8
changing success url
AdityaK2905 Mar 24, 2026
891c0a0
Auto-update feature branch with changes from the main branch
github-actions[bot] Mar 29, 2026
392d10d
Auto-update feature branch with changes from the main branch
github-actions[bot] Mar 30, 2026
4562c7b
removed double emailing and better email info
AdityaK2905 Mar 31, 2026
b0e5afd
fixing tests for previous change
AdityaK2905 Mar 31, 2026
3e3d2b7
fixing tests for previous change
AdityaK2905 Mar 31, 2026
ab34490
updated partial payment classification
AdityaK2905 Mar 31, 2026
ba44d61
test updates
AdityaK2905 Mar 31, 2026
3539656
added email for settled partial payment
AdityaK2905 Mar 31, 2026
e8f1296
added test for coverage
AdityaK2905 Mar 31, 2026
6e85200
Auto-update feature branch with changes from the main branch
github-actions[bot] Apr 1, 2026
ab1ef01
Auto-update feature branch with changes from the main branch
github-actions[bot] Apr 1, 2026
998da96
Auto-update feature branch with changes from the main branch
github-actions[bot] Apr 2, 2026
dbb9b43
Auto-update feature branch with changes from the main branch
github-actions[bot] Apr 2, 2026
4e13a53
Auto-update feature branch with changes from the main branch
github-actions[bot] Apr 3, 2026
ff6132b
Auto-update feature branch with changes from the main branch
github-actions[bot] Apr 3, 2026
24e24d1
Auto-update feature branch with changes from the main branch
github-actions[bot] Apr 4, 2026
c842820
Auto-update feature branch with changes from the main branch
github-actions[bot] Apr 5, 2026
356148f
status page
AdityaK2905 Apr 7, 2026
edc943f
updated to be unauthenticated status
AdityaK2905 Apr 7, 2026
ff10a6d
update
AdityaK2905 Apr 7, 2026
59836b4
redeploying
AdityaK2905 Apr 7, 2026
afa38ae
Auto-update feature branch with changes from the main branch
github-actions[bot] Apr 13, 2026
b9ba742
updating for qa to properly redirect to status page after payment
AdityaK2905 Apr 14, 2026
b4d1406
token redirects to /status
AdityaK2905 Apr 14, 2026
3f51146
increased test coverage
AdityaK2905 Apr 14, 2026
d3f355d
import path updated
AdityaK2905 Apr 14, 2026
6697e7c
Auto-update feature branch with changes from the main branch
github-actions[bot] Apr 14, 2026
825013f
adding UI for status page
AdityaK2905 Apr 14, 2026
f90b5cf
Auto-update feature branch with changes from the main branch
github-actions[bot] Apr 14, 2026
c8ad2f4
removed bug on stripe link page where org selector would not appear
AdityaK2905 Apr 14, 2026
cc93e8a
fixed stripe checkout function type
AdityaK2905 Apr 14, 2026
3d161e5
fix
AdityaK2905 Apr 14, 2026
db37885
updated test with proper error code
AdityaK2905 Apr 14, 2026
34a4079
updated test again..
AdityaK2905 Apr 14, 2026
7914d88
hitting /status directly
AdityaK2905 Apr 14, 2026
0c8a4c8
Auto-update feature branch with changes from the main branch
github-actions[bot] Apr 14, 2026
87ba858
Auto-update feature branch with changes from the main branch
github-actions[bot] Apr 20, 2026
6064f89
Auto-update feature branch with changes from the main branch
github-actions[bot] Apr 20, 2026
b910a46
Auto-update feature branch with changes from the main branch
github-actions[bot] Apr 20, 2026
592aa91
Auto-update feature branch with changes from the main branch
github-actions[bot] Apr 20, 2026
6d1b91b
fixed /status route to render proper information after stripe payment
AdityaK2905 Apr 20, 2026
d6b49fb
minor ui fixes on payment status page
AdityaK2905 Apr 21, 2026
7a790b1
Reversion
AdityaK2905 Apr 21, 2026
45ef153
reworked success url
AdityaK2905 Apr 21, 2026
1ad1d1a
ui fixes
AdityaK2905 Apr 21, 2026
28e395e
Auto-update feature branch with changes from the main branch
github-actions[bot] Apr 24, 2026
2d6e6fd
Auto-update feature branch with changes from the main branch
github-actions[bot] Apr 28, 2026
6b1fb14
Auto-update feature branch with changes from the main branch
github-actions[bot] Apr 30, 2026
d0ee870
Auto-update feature branch with changes from the main branch
github-actions[bot] Apr 30, 2026
8dd0147
Auto-update feature branch with changes from the main branch
github-actions[bot] Apr 30, 2026
a404ae7
bug fixes on checkout
AdityaK2905 May 5, 2026
c4ab4f9
removed unused variable
AdityaK2905 May 5, 2026
18870cd
tests
AdityaK2905 May 5, 2026
aa6830b
lint
AdityaK2905 May 5, 2026
be27e24
reworked status page + partial payments
AdityaK2905 May 5, 2026
c175e74
added tests for coverage
AdityaK2905 May 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
392 changes: 385 additions & 7 deletions src/api/functions/stripe.ts

Large diffs are not rendered by default.

1,170 changes: 1,024 additions & 146 deletions src/api/routes/stripe.ts

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions src/common/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type ConfigType = {
AadValidClientId: string;
EntraServicePrincipalId: string;
LinkryBaseUrl: string;
PaymentBaseUrl: string;
PasskitIdentifier: string;
PasskitSerialNumber: string;
EmailDomain: string;
Expand Down Expand Up @@ -143,6 +144,7 @@ const environmentConfig: EnvironmentConfigType = {
],
AadValidClientId: "39c28870-94e4-47ee-b4fb-affe0bf96c9f",
LinkryBaseUrl: "https://core.aws.qa.acmuiuc.org",
PaymentBaseUrl: "https://invoice.aws.qa.acmuiuc.org",
PasskitIdentifier: "pass.org.acmuiuc.qa.membership",
PasskitSerialNumber: "0",
EmailDomain: "aws.qa.acmuiuc.org",
Expand Down Expand Up @@ -187,6 +189,7 @@ const environmentConfig: EnvironmentConfigType = {
],
AadValidClientId: "5e08cf0f-53bb-4e09-9df2-e9bdc3467296",
LinkryBaseUrl: "https://acm.gg/",
PaymentBaseUrl: "https://invoice.acm.illinois.edu",
PasskitIdentifier: "pass.edu.illinois.acm.membership",
PasskitSerialNumber: "0",
EmailDomain: "acm.illinois.edu",
Expand Down
50 changes: 49 additions & 1 deletion src/common/types/stripe.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as z from "zod/v4";

import { OrgUniqueId } from "./generic.js"

const id = z.string().min(1).meta({
description: "The Payment Link's ID in the Stripe API",
Expand Down Expand Up @@ -43,8 +43,56 @@ export const invoiceLinkGetResponseSchema = z.array(
invoiceId,
invoiceAmountUsd,
createdAt: z.union([z.iso.datetime(), z.null()]).meta({ description: "When the payment link was created." })
}).omit({
contactEmail: true,
contactName: true
})
);

export type GetInvoiceLinksResponse = z.infer<
typeof invoiceLinkGetResponseSchema>;

export const createInvoicePostResponseSchema = z.object({
id: z.string().min(1),
invoiceId: z.string().min(1),
link: z.url(),
});

export const createInvoiceConflictResponseSchema = z.object({
needsConfirmation: z.literal(true),
customerId: z.string().min(1),
current: z.object({
name: z.string().nullable().optional(),
email: z.string().nullable().optional(),
}),
incoming: z.object({
name: z.string().min(1),
email: z.email(),
}),
message: z.string().min(1),
});

export const createInvoicePostResponseSchemaUnion = z.union([
createInvoicePostResponseSchema, // success: 201
createInvoiceConflictResponseSchema, // info mismatch: 409
]);

export type PostCreateInvoiceResponseUnion = z.infer<
typeof createInvoicePostResponseSchemaUnion
>;

export const createInvoicePostRequestSchema = z.object({
invoiceId,
invoiceAmountUsd,
contactName: z.string().min(1),
contactEmail: z.email(),
acmOrg: OrgUniqueId,
});

export type PostCreateInvoiceRequest = z.infer<
typeof createInvoicePostRequestSchema
>;

export type PostCreateInvoiceResponse = z.infer<
typeof createInvoicePostResponseSchema
>;
47 changes: 47 additions & 0 deletions src/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,50 @@ export function getNetIdFromEmail(email: string): string {
const [netId] = normalizedEmail.split("@");
return netId.toLowerCase();
}


/**
* Encodes an invoice payment token in the format:
* Base64URL(orgId#emailDomain#invoiceId)
*/
export function encodeInvoiceToken({
orgId,
emailDomain,
invoiceId,
}: {
orgId: string;
emailDomain: string;
invoiceId: string;
}): string {
return Buffer.from(
`${orgId}#${emailDomain}#${invoiceId}`,
"utf8",
).toString("base64url");
}

/**
* Decodes and validates an invoice payment token.
*/
export function decodeInvoiceToken(token: string): {
orgId: string;
emailDomain: string;
invoiceId: string;
} {
let decoded: string;

try {
decoded = Buffer.from(token, "base64url").toString("utf8");
} catch {
throw new ValidationError({ message: "Invalid invoice token encoding." });
}

const parts = decoded.split("#");
const orgId = parts[0];
const emailDomain = parts[1];
const invoiceId = parts.slice(2).join("#"); // keep remainder

if (!orgId || !emailDomain || !invoiceId) {
throw new ValidationError({ message: "Malformed invoice token." });
}
return { orgId, emailDomain, invoiceId };
Comment on lines +110 to +131
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify that Buffer.from with base64url does not throw on invalid input
node -e "
try {
  const result = Buffer.from('!!!not-valid-base64url!!!', 'base64url').toString('utf8');
  console.log('Did NOT throw. Result:', JSON.stringify(result));
} catch (e) {
  console.log('Threw:', e.message);
}
"

Repository: acm-uiuc/core

Length of output: 93


Remove or replace the ineffective try/catch block for Buffer.from.

Buffer.from(token, "base64url") does not throw on malformed input — it silently returns a best-effort decode with garbage bytes. The catch block (lines 119–121) will never execute, and the error message "Invalid invoice token encoding" is unreachable. The structural validation on lines 128–130 already catches malformed tokens, making this error handler redundant. Either remove the try/catch or replace it with actual validation logic that detects invalid base64url encoding.

🤖 Prompt for AI Agents
In `@src/common/utils.ts` around lines 110 - 131, In decodeInvoiceToken, remove
the ineffective try/catch around Buffer.from(token, "base64url") and add real
validation: verify the input is valid base64url by either (A) testing the token
against a base64url pattern (e.g. only A-Z a-z 0-9 - _ and optional padding)
before decoding, or (B) perform a round-trip check after decoding (re-encode the
decoded UTF-8 string with Buffer.from(decoded, "utf8").toString("base64url") and
ensure it equals the original token) so malformed base64url is detected and
causes the ValidationError; keep the existing structural checks on
parts/orgId/emailDomain/invoiceId and throw the same ValidationError when
validation fails.

}
13 changes: 13 additions & 0 deletions src/ui/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { OrgInfoPage } from "./pages/organization/OrgInfo.page";
import { ViewStoreItemsPage } from "./pages/store/ViewStoreItems.page";
import { ViewStorePurchasesPage } from "./pages/store/ViewStorePurchases.page";
import { ManageRsvpConfigFormPage } from "./pages/rsvps/ManageRsvpConfig.page";
import { StripePaymentStatus } from "./pages/stripe/StripePaymentStatus.page";

const ProfileRediect: React.FC = () => {
const location = useLocation();
Expand Down Expand Up @@ -106,6 +107,10 @@ const profileRouter = createBrowserRouter([
path: "/profile",
element: <ViewProfilePage />,
},
{
path: "/stripe/status",
element: <StripePaymentStatus />,
},
{
path: "*",
element: <ProfileRediect />,
Expand All @@ -132,6 +137,10 @@ const unauthenticatedRouter = createBrowserRouter([
path: "*",
element: <LoginRedirect />,
},
{
path: "/stripe/status",
element: <StripePaymentStatus />,
},
],
},
]);
Expand Down Expand Up @@ -222,6 +231,10 @@ const authenticatedRouter = createBrowserRouter([
path: "/stripe",
element: <ManageStripeLinksPage />,
},
{
path: "/stripe/status",
element: <StripePaymentStatus />,
},
{
path: "/roomRequests",
element: <ManageRoomRequestsPage />,
Expand Down
11 changes: 6 additions & 5 deletions src/ui/pages/stripe/CreateLink.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe("StripeCreateLinkPanel Tests", () => {
it("renders the form fields correctly", async () => {
await renderComponent();

expect(screen.getByText("Invoice ID")).toBeInTheDocument();
expect(screen.getByPlaceholderText("123")).toBeInTheDocument();
expect(screen.getByText("Invoice Amount")).toBeInTheDocument();
expect(screen.getByText("Invoice Recipient Name")).toBeInTheDocument();
expect(screen.getByText("Invoice Recipient Email")).toBeInTheDocument();
Expand All @@ -51,7 +51,7 @@ describe("StripeCreateLinkPanel Tests", () => {
screen.getByPlaceholderText("email@illinois.edu"),
"invalidEmail",
);
await user.clear(screen.getByPlaceholderText("ACM100"));
await user.clear(screen.getByPlaceholderText("123"));
expect(createLinkMock).toHaveBeenCalledTimes(0);
});

Expand All @@ -60,7 +60,7 @@ describe("StripeCreateLinkPanel Tests", () => {
const user = userEvent.setup();
await renderComponent();

await user.type(screen.getByPlaceholderText("ACM100"), "INV123");
await user.type(screen.getByPlaceholderText("123"), "INV123");
await user.clear(screen.getByPlaceholderText("100"));
await user.type(screen.getByPlaceholderText("100"), "100");
await user.type(screen.getByPlaceholderText("John Doe"), "John Doe");
Expand All @@ -73,6 +73,7 @@ describe("StripeCreateLinkPanel Tests", () => {
await act(async () => {
expect(createLinkMock).toHaveBeenCalledWith({
achPaymentsEnabled: false,
acmOrg: null,
invoiceId: "INV123",
invoiceAmountUsd: 100,
contactName: "John Doe",
Expand All @@ -86,7 +87,7 @@ describe("StripeCreateLinkPanel Tests", () => {
const user = userEvent.setup();
await renderComponent();

await user.type(screen.getByPlaceholderText("ACM100"), "INV123");
await user.type(screen.getByPlaceholderText("123"), "INV123");
await user.type(screen.getByPlaceholderText("100"), "100");
await user.type(screen.getByPlaceholderText("John Doe"), "John Doe");
await user.type(
Expand All @@ -107,7 +108,7 @@ describe("StripeCreateLinkPanel Tests", () => {
const user = userEvent.setup();
await renderComponent();

await user.type(screen.getByPlaceholderText("ACM100"), "INV123");
await user.type(screen.getByPlaceholderText("123"), "INV123");
await user.type(screen.getByPlaceholderText("100"), "100");
await user.type(screen.getByPlaceholderText("John Doe"), "John Doe");
await user.type(
Expand Down
32 changes: 30 additions & 2 deletions src/ui/pages/stripe/CreateLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ import {
PostInvoiceLinkRequest,
PostInvoiceLinkResponse,
} from "@common/types/stripe";
import FullScreenLoader from "@ui/components/AuthContext/LoadingScreen";
import { ManageableOrgsSelector } from "@ui/components/ManageableOrgsSelector";
import { getPrimarySuggestedOrg } from "@ui/util";
import { useAuth } from "@ui/components/AuthContext";
import { OrganizationId } from "@acm-uiuc/js-shared";
import { AppRoles } from "@common/roles";

interface StripeCreateLinkPanelProps {
createLink: (
Expand All @@ -36,6 +40,10 @@ export const StripeCreateLinkPanel: React.FC<StripeCreateLinkPanelProps> = ({
const [modalOpened, setModalOpened] = useState(false);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [returnedLink, setReturnedLink] = useState<string | null>(null);
const [userPrimaryOrg, setUserPrimaryOrg] = useState<OrganizationId | null>(
null,
);
const { orgRoles } = useAuth();

const form = useForm({
initialValues: {
Expand All @@ -44,6 +52,7 @@ export const StripeCreateLinkPanel: React.FC<StripeCreateLinkPanelProps> = ({
contactName: "",
contactEmail: "",
achPaymentsEnabled: false,
acmOrg: userPrimaryOrg,
},
validate: {
invoiceId: (value) =>
Expand Down Expand Up @@ -86,9 +95,28 @@ export const StripeCreateLinkPanel: React.FC<StripeCreateLinkPanelProps> = ({
Create a Payment Link
</Title>
<form onSubmit={form.onSubmit(handleSubmit)}>
<ManageableOrgsSelector
adminRoles={[AppRoles.STRIPE_LINK_ADMIN]}
showAllOrgs={false}
value={form.values.acmOrg ?? null}
onChange={(org) => form.setFieldValue("acmOrg", org)}
onOrgsLoaded={(orgs) => {
if (orgs.length > 0) {
const primOrg = getPrimarySuggestedOrg(orgRoles);
if (primOrg) {
setUserPrimaryOrg(primOrg);
form.setFieldValue("acmOrg", primOrg);
}
}
}}
label="Invoice Recipient Org"
placeholder="Select recipient organization"
description="Only orgs you manage are shown here."
withAsterisk
/>
<TextInput
label="Invoice ID"
placeholder="ACM100"
placeholder="123"
description="Make sure the Invoice ID is prefixed with a unique string for your group to avoid processing delays."
{...form.getInputProps("invoiceId")}
required
Expand Down
Loading
Loading