Skip to content

Commit cdc466d

Browse files
authored
Disable stripe links if full payment is made (#165)
1 parent 57208e3 commit cdc466d

File tree

8 files changed

+276
-32
lines changed

8 files changed

+276
-32
lines changed

.github/workflows/deploy-dev.yml

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ on:
88
- main
99

1010
jobs:
11-
test-unit:
11+
test:
1212
runs-on: ubuntu-latest
1313
name: Run Unit Tests
1414
steps:
@@ -20,7 +20,7 @@ jobs:
2020
uses: actions/setup-node@v4
2121
with:
2222
node-version: 22.x
23-
cache: 'yarn'
23+
cache: "yarn"
2424

2525
- name: Restore Yarn Cache
2626
uses: actions/cache@v4
@@ -30,14 +30,47 @@ jobs:
3030
restore-keys: |
3131
yarn-modules-${{ runner.os }}-
3232
33-
- name: Set up Python 3.11 for testing
34-
uses: actions/setup-python@v5
35-
with:
36-
python-version: 3.11
37-
3833
- name: Run unit testing
3934
run: make test_unit
4035

36+
build:
37+
runs-on: ubuntu-latest
38+
name: Build Application
39+
steps:
40+
- uses: actions/checkout@v4
41+
env:
42+
HUSKY: "0"
43+
44+
- name: Set up Node
45+
uses: actions/setup-node@v4
46+
with:
47+
node-version: 22.x
48+
cache: "yarn"
49+
50+
- name: Restore Yarn Cache
51+
uses: actions/cache@v4
52+
with:
53+
path: node_modules
54+
key: yarn-modules-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}-dev
55+
restore-keys: |
56+
yarn-modules-${{ runner.os }}-
57+
58+
- name: Run build
59+
run: make build
60+
env:
61+
HUSKY: "0"
62+
VITE_RUN_ENVIRONMENT: dev
63+
64+
- name: Upload Build files
65+
uses: actions/upload-artifact@v4
66+
with:
67+
include-hidden-files: true
68+
name: build
69+
path: |
70+
.aws-sam/
71+
dist/
72+
dist_ui/
73+
4174
deploy-test-dev:
4275
runs-on: ubuntu-latest
4376
permissions:
@@ -49,7 +82,8 @@ jobs:
4982
environment: "AWS DEV"
5083
name: Deploy to DEV and Run Tests
5184
needs:
52-
- test-unit
85+
- test
86+
- build
5387
steps:
5488
- uses: actions/checkout@v4
5589
env:
@@ -59,7 +93,7 @@ jobs:
5993
uses: actions/setup-node@v4
6094
with:
6195
node-version: 22.x
62-
cache: 'yarn'
96+
cache: "yarn"
6397

6498
- name: Restore Yarn Cache
6599
uses: actions/cache@v4
@@ -69,6 +103,11 @@ jobs:
69103
restore-keys: |
70104
yarn-modules-${{ runner.os }}-
71105
106+
- name: Download Build files
107+
uses: actions/download-artifact@v4
108+
with:
109+
name: build
110+
72111
- uses: aws-actions/setup-sam@v2
73112
with:
74113
use-installer: true

.github/workflows/deploy-prod.yml

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,68 @@ on:
77
branches:
88
- main
99
jobs:
10-
test-unit:
10+
test:
1111
runs-on: ubuntu-latest
1212
name: Run Unit Tests
1313
steps:
14+
- uses: actions/checkout@v4
15+
env:
16+
HUSKY: "0"
17+
1418
- name: Set up Node
1519
uses: actions/setup-node@v4
1620
with:
1721
node-version: 22.x
22+
cache: "yarn"
23+
24+
- name: Restore Yarn Cache
25+
uses: actions/cache@v4
26+
with:
27+
path: node_modules
28+
key: yarn-modules-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}-dev
29+
restore-keys: |
30+
yarn-modules-${{ runner.os }}-
31+
32+
- name: Run unit testing
33+
run: make test_unit
34+
35+
build:
36+
runs-on: ubuntu-latest
37+
name: Build Application
38+
steps:
1839
- uses: actions/checkout@v4
1940
env:
2041
HUSKY: "0"
21-
- name: Set up Python 3.11 for testing
22-
uses: actions/setup-python@v5
42+
43+
- name: Set up Node
44+
uses: actions/setup-node@v4
2345
with:
24-
python-version: 3.11
25-
- name: Run unit testing
26-
run: make test_unit
46+
node-version: 22.x
47+
cache: "yarn"
48+
49+
- name: Restore Yarn Cache
50+
uses: actions/cache@v4
51+
with:
52+
path: node_modules
53+
key: yarn-modules-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}-prod
54+
restore-keys: |
55+
yarn-modules-${{ runner.os }}-
56+
57+
- name: Run build
58+
run: make build
59+
env:
60+
HUSKY: "0"
61+
VITE_RUN_ENVIRONMENT: prod
62+
63+
- name: Upload Build files
64+
uses: actions/upload-artifact@v4
65+
with:
66+
include-hidden-files: true
67+
name: build-prod
68+
path: |
69+
.aws-sam/
70+
dist/
71+
dist_ui/
2772
2873
deploy-prod:
2974
runs-on: ubuntu-latest
@@ -35,7 +80,8 @@ jobs:
3580
id-token: write
3681
contents: read
3782
needs:
38-
- test-unit
83+
- test
84+
- build
3985
environment: "AWS PROD"
4086
steps:
4187
- name: Set up Node for testing
@@ -53,6 +99,10 @@ jobs:
5399
uses: actions/setup-python@v5
54100
with:
55101
python-version: 3.11
102+
- name: Download Build files
103+
uses: actions/download-artifact@v4
104+
with:
105+
name: build-prod
56106
- uses: aws-actions/configure-aws-credentials@v4
57107
with:
58108
role-to-assume: arn:aws:iam::298118738376:role/GitHubActionsRole

Makefile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ clean:
5252
rm -rf dist/
5353
rm -rf dist_ui/
5454
rm -rf dist_devel/
55+
rm -rf coverage/
5556

5657
build: src/ cloudformation/ docs/
5758
yarn -D
@@ -65,14 +66,14 @@ build: src/ cloudformation/ docs/
6566
local:
6667
VITE_BUILD_HASH=$(GIT_HASH) yarn run dev
6768

68-
deploy_prod: check_account_prod build
69+
deploy_prod: check_account_prod
6970
@echo "Deploying CloudFormation stack..."
7071
sam deploy $(common_params) --parameter-overrides $(run_env)=prod $(set_application_prefix)=$(application_key) $(set_application_name)="$(application_name)" S3BucketPrefix="$(s3_bucket_prefix)"
7172
@echo "Syncing S3 bucket..."
7273
aws s3 sync $(dist_ui_directory_root) s3://$(ui_s3_bucket)/ --delete
7374
make invalidate_cloudfront
7475

75-
deploy_dev: check_account_dev build
76+
deploy_dev: check_account_dev
7677
@echo "Deploying CloudFormation stack..."
7778
sam deploy $(common_params) --parameter-overrides $(run_env)=dev $(set_application_prefix)=$(application_key) $(set_application_name)="$(application_name)" S3BucketPrefix="$(s3_bucket_prefix)"
7879
@echo "Syncing S3 bucket..."

src/api/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"fastify-plugin": "^5.0.1",
4545
"fastify-raw-body": "^5.0.0",
4646
"fastify-zod-openapi": "^4.1.1",
47+
"handlebars": "^4.7.8",
4748
"ical-generator": "^8.1.1",
4849
"ioredis": "^5.6.1",
4950
"jsonwebtoken": "^9.0.2",

src/api/routes/stripe.ts

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
QueryCommand,
33
ScanCommand,
44
TransactWriteItemsCommand,
5+
UpdateItemCommand,
56
} from "@aws-sdk/client-dynamodb";
67
import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
78
import { withRoles, withTags } from "api/components/index.js";
@@ -190,16 +191,16 @@ const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => {
190191
if (!request.rawBody) {
191192
throw new ValidationError({ message: "Could not get raw body." });
192193
}
194+
const secretApiConfig =
195+
(await getSecretValue(
196+
fastify.secretsManagerClient,
197+
genericConfig.ConfigSecretName,
198+
)) || {};
193199
try {
194200
const sig = request.headers["stripe-signature"];
195201
if (!sig || typeof sig !== "string") {
196202
throw new Error("Missing or invalid Stripe signature");
197203
}
198-
const secretApiConfig =
199-
(await getSecretValue(
200-
fastify.secretsManagerClient,
201-
genericConfig.ConfigSecretName,
202-
)) || {};
203204
if (!secretApiConfig) {
204205
throw new InternalServerError({
205206
message: "Could not connect to Stripe.",
@@ -259,13 +260,17 @@ const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => {
259260
const unmarshalledEntry = unmarshall(response.Items[0]) as {
260261
userId: string;
261262
invoiceId: string;
263+
amount: number;
264+
priceId: string;
265+
productId: string;
262266
};
263267
if (!unmarshalledEntry.userId || !unmarshalledEntry.invoiceId) {
264268
return reply.status(200).send({
265269
handled: false,
266270
requestId: request.id,
267271
});
268272
}
273+
const paidInFull = paymentAmount === unmarshalledEntry.amount;
269274
const withCurrency = new Intl.NumberFormat("en-US", {
270275
style: "currency",
271276
currency: paymentCurrency.toUpperCase(),
@@ -274,8 +279,10 @@ const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => {
274279
.map((val) => val.value)
275280
.join("");
276281
request.log.info(
277-
`Registered payment of ${withCurrency} by ${name} (${email}) for payment link ${paymentLinkId} invoice ID ${unmarshalledEntry.invoiceId}).`,
282+
`Registered payment of ${withCurrency} by ${name} (${email}) for payment link ${paymentLinkId} invoice ID ${unmarshalledEntry.invoiceId}). Invoice was paid ${paidInFull ? "in full." : "partially."}`,
278283
);
284+
// Notify link owner of payment
285+
let queueId;
279286
if (unmarshalledEntry.userId.includes("@")) {
280287
request.log.info(
281288
`Sending email to ${unmarshalledEntry.userId}...`,
@@ -290,7 +297,7 @@ const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => {
290297
payload: {
291298
to: [unmarshalledEntry.userId],
292299
subject: `Payment Recieved for Invoice ${unmarshalledEntry.invoiceId}`,
293-
content: `Received payment of ${withCurrency} by ${name} (${email}) for Invoice ${unmarshalledEntry.invoiceId}. Please contact [email protected] with any questions.`,
300+
content: `ACM @ UIUC has received ${paidInFull ? "full" : "partial"} payment for Invoice ${unmarshalledEntry.invoiceId} (${withCurrency} by ${name}, ${email}).\n\nPlease contact Officer Board with any questions.`,
294301
},
295302
};
296303
if (!fastify.sqsClient) {
@@ -304,15 +311,50 @@ const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => {
304311
MessageBody: JSON.stringify(sqsPayload),
305312
}),
306313
);
307-
return reply.status(200).send({
308-
handled: true,
309-
requestId: request.id,
310-
queueId: result.MessageId,
314+
queueId = result.MessageId || "";
315+
}
316+
// If full payment is done, disable the link
317+
if (paidInFull) {
318+
request.log.debug("Paid in full, disabling link.");
319+
const logStatement = buildAuditLogTransactPut({
320+
entry: {
321+
module: Modules.STRIPE,
322+
actor: eventId,
323+
target: `Link ${paymentLinkId} | Invoice ${unmarshalledEntry.invoiceId}`,
324+
message:
325+
"Disabled Stripe payment link as payment was made in full.",
326+
},
327+
});
328+
const dynamoCommand = new TransactWriteItemsCommand({
329+
TransactItems: [
330+
logStatement,
331+
{
332+
Update: {
333+
TableName: genericConfig.StripeLinksDynamoTableName,
334+
Key: {
335+
userId: { S: unmarshalledEntry.userId },
336+
linkId: { S: paymentLinkId },
337+
},
338+
UpdateExpression: "SET active = :new_val",
339+
ConditionExpression: "active = :old_val",
340+
ExpressionAttributeValues: {
341+
":new_val": { BOOL: false },
342+
":old_val": { BOOL: true },
343+
},
344+
},
345+
},
346+
],
347+
});
348+
await deactivateStripeLink({
349+
stripeApiKey: secretApiConfig.stripe_secret_key as string,
350+
linkId: paymentLinkId,
311351
});
352+
await fastify.dynamoClient.send(dynamoCommand);
312353
}
313354
return reply.status(200).send({
314355
handled: true,
315356
requestId: request.id,
357+
queueId: queueId || "",
316358
});
317359
}
318360
return reply

src/api/sqs/handlers/emailNotifications.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@ import { SendEmailCommand, SESClient } from "@aws-sdk/client-ses";
44
import { genericConfig } from "common/config.js";
55
import { createAuditLogEntry } from "api/functions/auditLog.js";
66
import { Modules } from "common/modules.js";
7+
import Handlebars from "handlebars";
8+
import emailTemplate from "./templates/notification.js";
9+
10+
Handlebars.registerHelper("nl2br", (text) => {
11+
let nl2br = `${text}`.replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, "$1<br>$2");
12+
nl2br = `<p>${nl2br.replace(/<br>/g, "</p><p>")}</p>`;
13+
return new Handlebars.SafeString(nl2br);
14+
});
15+
16+
const compiledTemplate = Handlebars.compile(emailTemplate);
717

818
const stripHtml = (html: string): string => {
919
return html
@@ -17,7 +27,8 @@ export const emailNotificationsHandler: SQSHandlerFunction<
1727
AvailableSQSFunctions.EmailNotifications
1828
> = async (payload, metadata, logger) => {
1929
const { to, cc, bcc, content, subject } = payload;
20-
const senderEmail = `ACM @ UIUC <notifications@${currentEnvironmentConfig.EmailDomain}>`;
30+
const senderEmailAddress = `notifications@${currentEnvironmentConfig.EmailDomain}`;
31+
const senderEmail = `ACM @ UIUC <${senderEmailAddress}>`;
2132
logger.info("Constructing email...");
2233
const command = new SendEmailCommand({
2334
Source: senderEmail,
@@ -33,7 +44,12 @@ export const emailNotificationsHandler: SQSHandlerFunction<
3344
},
3445
Body: {
3546
Html: {
36-
Data: content,
47+
Data: compiledTemplate({
48+
...payload,
49+
id: metadata.reqId,
50+
from: senderEmailAddress,
51+
currentYear: new Date().getFullYear(),
52+
}),
3753
Charset: "UTF-8",
3854
},
3955
Text: {

0 commit comments

Comments
 (0)