Skip to content

Commit 0b8eee9

Browse files
committed
feat: add reminder emails for collection system
1 parent 0ceb6dc commit 0b8eee9

File tree

12 files changed

+313
-0
lines changed

12 files changed

+313
-0
lines changed

collection/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,16 @@ Other tools have their own package.jsons as that was how they were originally se
3030
# Docker
3131

3232
To build the docker image, run `npx nx container collection` from the _root_ of the repo.
33+
34+
# What `emails/` for?
35+
36+
This is for sending reminder emails to those who haven't collected their merch yet.
37+
38+
Usage:
39+
40+
```bash
41+
cd emails
42+
npx tsx getUncollectedPeople.ts # outputs a JSON file to output/reminders.json
43+
docsoc-mailmerge generate nunjucks ./data/reminders.json ./templates/reminder.njk -s json -o output -n reminders
44+
# then send (check README in emails folder)!
45+
```

collection/emails/.env.template

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Fill these in to send emails
2+
DOCSOC_SMTP_SERVER=smtp-mail.outlook.com
3+
DOCSOC_SMTP_PORT=587
4+
DOCSOC_OUTLOOK_USERNAME=
5+
# Password to docsoc email
6+
DOCSOC_OUTLOOK_PASSWORD=
7+
8+
# Optional: Fill these in to uplod drafts
9+
# You will need to create an app registration in Entra ID, restricted to the organisation,
10+
# And grant it the following permissions:
11+
# - Mail.ReadWrite
12+
# - User.Read
13+
MS_ENTRA_CLIENT_ID=
14+
MS_ENTRA_CLIENT_SECRET=
15+
MS_ENTRA_TENANT_ID=
16+
17+
# DB URL for use by getUncollectedPeople
18+
COLLECTION_DATABASE_URL=

collection/emails/.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
attachments/*
2+
!attachments/.gitkeep
3+
4+
data/*
5+
!data/.gitkeep
6+
!data/example.csv
7+
8+
output/*
9+
!output/.gitkeep

collection/emails/README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
To get started:
2+
3+
1. Put your own CSV in the `data` folder, with at the minimum `to` and `subject` columns.
4+
1. You can also add `cc` and `bcc` columns (to use them you will need to pass the correct CLI option though)
5+
2. `to`, `cc`, and `bcc` can be a space-separated list of emails.
6+
3. You can add any other columns you like, and they will be available in the template.
7+
4. For attachments, add a column with the name `attachment` with a singular path to the file to attach relative to th workspace root (e.g. `./attachments/image1.jpg`).
8+
1. Or, pass the same attachment to every email using the `-a` flag to `generate`
9+
5. For multiple attachments, have separate columns e.g. `attachment1`, `attachment2`, etc.
10+
6. See `data/example.csv` for an example.
11+
2. Put your own nunjucks markdown email template in the `templates` folder.
12+
1. You can also edit the default `wrapper.html.njk` file - this is what the markdown HTML will be wrapped in when sending it. It muat _always_ include a `{{ content }}` tag, which will be replaced with the markdown HTML.
13+
3. Fill in the `.env` file with your email credentials.
14+
15+
Then run the following commands:
16+
17+
```bash
18+
docsoc-mailmerge generate nunjucks ./data/my-data.csv ./templates/my-template.md.njk -o ./output --htmlTemplate ./templates/wrapper.html.njk
19+
# make some edits to the outputs and regenerate them:
20+
docsoc-mailmerge regenerate ./output/<runname>
21+
# review them, then send:
22+
docsoc-mailmerge send ./output/<runname>
23+
```
24+
25+
The CLI tool has many options - use `--help` to see them all:
26+
27+
```bash
28+
docsoc-mailmerge generate nunjucks --help
29+
docsoc-mailmerge regenerate --help
30+
docsoc-mailmerge send --help
31+
```
32+
33+
## What happen when you generate
34+
35+
1. Each record in the CSV will result in 3 files in `./output/<runname>`: an editable markdown file to allow you to modify the email, a HTML rendering of the markdown that you should not edit, and a `.json` metadata file
36+
2. The HTML files, which is what is actually sent, can be regenerated after edting the markdown files with `regenerate` command (see below)
37+
3. If you want to edit the to address or subject after this point you will need to edit the JSON files; csv edits are ignored. If you edit the CSV, delete all outputs and run generate again.
38+
39+
## If the .env file is missing
40+
41+
Use this template:
42+
43+
```bash
44+
# Fill these in to send emails
45+
DOCSOC_SMTP_SERVER=smtp-mail.outlook.com
46+
DOCSOC_SMTP_PORT=587
47+
DOCSOC_OUTLOOK_USERNAME=
48+
# Password to docsoc email
49+
DOCSOC_OUTLOOK_PASSWORD=
50+
51+
# Optional: Fill these in to uplod drafts
52+
# You will need to create an app registration in Entra ID, restricted to the organisation,
53+
# And grant it the following permissions:
54+
# - Mail.ReadWrite
55+
# - User.Read
56+
MS_ENTRA_CLIENT_ID=
57+
MS_ENTRA_CLIENT_SECRET=
58+
MS_ENTRA_TENANT_ID=
59+
```

collection/emails/attachments/.gitkeep

Whitespace-only changes.

collection/emails/data/.gitkeep

Whitespace-only changes.

collection/emails/data/example.csv

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
name,to,subject,attachment1,attachment2,bcc,cc
2+
Example Person,email1@example.com email2@example.co.uk,Example subject,./attachments/image1.jpg,./attachments/image2.jpg,bcc@london.com,cc@must-cc.org otherperson@example.com
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/**
2+
* Outputs a JSON file with the students who have not collected their merch, for use by the mailmerge tool
3+
* to send a reminder to these people.
4+
*
5+
* Created with co-pilot.
6+
*/
7+
import { PrismaClient } from "@prisma/client";
8+
// Load .env file ".env"
9+
import dotenv from "dotenv";
10+
import fs from "fs/promises";
11+
12+
dotenv.config();
13+
14+
const prisma = new PrismaClient();
15+
16+
interface OutputRecord {
17+
to: string;
18+
name: string;
19+
shortcode: string;
20+
itemsToCollect: {
21+
rootitem: string;
22+
variant: string;
23+
quantity: number;
24+
}[];
25+
subject: string;
26+
}
27+
28+
async function getUncollectedPeople() {
29+
const uncollectedOrders = await prisma.orderItem.findMany({
30+
where: { collected: false },
31+
include: {
32+
Order: {
33+
include: {
34+
ImperialStudent: true,
35+
},
36+
},
37+
Variant: {
38+
include: {
39+
RootItem: true,
40+
},
41+
},
42+
},
43+
});
44+
45+
const outputRecords: OutputRecord[] = [];
46+
47+
const studentMap = new Map<string, OutputRecord>();
48+
49+
for (const orderItem of uncollectedOrders) {
50+
const student = orderItem.Order.ImperialStudent;
51+
const studentKey = student.email;
52+
53+
// Filter out if rootitem is "Pride Lanyard"
54+
// (in 2024 we started handing out these lanyard to whoever after the summer collection so
55+
/// it was likely that anyway who didn't collect it during merch collections got them during the random hand outs we did)
56+
if (orderItem.Variant.RootItem.name === "Pride Lanyard") {
57+
console.log(`Skipping ${studentKey} for Pride Lanyard`);
58+
continue;
59+
}
60+
61+
if (!studentMap.has(studentKey)) {
62+
studentMap.set(studentKey, {
63+
to: student.email,
64+
name: `${student.firstName} ${student.lastName}`,
65+
shortcode: student.shortcode,
66+
itemsToCollect: [],
67+
subject: "Collect your remaining DoCSoc Summer Merch",
68+
});
69+
}
70+
71+
const studentRecord = studentMap.get(studentKey)!;
72+
studentRecord.itemsToCollect.push({
73+
rootitem: orderItem.Variant.RootItem.name,
74+
variant: orderItem.Variant.variantName,
75+
quantity: orderItem.quantity,
76+
});
77+
}
78+
79+
outputRecords.push(...studentMap.values());
80+
81+
await fs.writeFile("data/reminders.json", JSON.stringify(outputRecords, null, 2));
82+
}
83+
84+
getUncollectedPeople()
85+
.catch((e) => {
86+
console.error(e);
87+
})
88+
.finally(async () => {
89+
await prisma.$disconnect();
90+
});

collection/emails/mergeJay.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/**
2+
* You can safely ignore this file. It is used to merge the data from Jay's dump with the local database.
3+
*
4+
* Jay was a webmaster who got a old version of this system working on their machine, and we needed to import the collections from that DB into the working copy.
5+
*/
6+
import { PrismaClient } from "@prisma/client";
7+
import { parse } from "csv-parse";
8+
import dotenv from "dotenv";
9+
import fs from "fs/promises";
10+
11+
dotenv.config();
12+
13+
const prisma = new PrismaClient();
14+
15+
async function getStudentShortcode(orderItemId: number) {
16+
const orderItem = await prisma.orderItem.findUnique({
17+
where: { id: orderItemId },
18+
include: {
19+
Order: {
20+
include: {
21+
ImperialStudent: true,
22+
},
23+
},
24+
},
25+
});
26+
27+
if (!orderItem) {
28+
throw new Error(`OrderItem with ID ${orderItemId} does not exist.`);
29+
}
30+
31+
const studentShortcode = orderItem.Order.ImperialStudent.shortcode;
32+
return studentShortcode;
33+
}
34+
35+
async function mergeJayDump() {
36+
// Load Jay's dump
37+
const jayDumpContent = await fs.readFile("data/jay-dump.csv", "utf-8");
38+
const jayDumpRecords = await parse(jayDumpContent, {
39+
columns: true,
40+
delimiter: "\t",
41+
});
42+
43+
for await (const record of jayDumpRecords) {
44+
const orderItemId = parseInt(record.id, 10);
45+
const jayCollected = record.collected === "t";
46+
47+
// Check if the order item exists in the local database
48+
const localOrderItem = await prisma.orderItem.findUnique({
49+
where: { id: orderItemId },
50+
});
51+
52+
if (!localOrderItem) {
53+
// throw new Error(
54+
// `OrderItem with ID ${orderItemId} does not exist in the local database.`,
55+
// );
56+
console.error(`OrderItem with ID ${orderItemId} does not exist in the local database!`);
57+
continue;
58+
}
59+
60+
if (jayCollected && !localOrderItem.collected) {
61+
// Update the local database to mark the item as collected
62+
// await prisma.orderItem.update({
63+
// where: { id: orderItemId },
64+
// data: { collected: true },
65+
// });
66+
console.warn(
67+
`JAY TRUE, KISHAN FALSE: OrderItem with ID ${orderItemId} marked as collected in the local database. Shortcode: ${await getStudentShortcode(
68+
orderItemId,
69+
)}`,
70+
);
71+
72+
// Print shortcode of the student
73+
}
74+
75+
// if I have it marked as true, but Jay has it marked as false, report this
76+
if (localOrderItem.collected && !jayCollected) {
77+
console.warn(
78+
`KISHAN TRUE, JAY FALSE: OrderItem with ID ${orderItemId} marked as collected in the local database, but not in Jay's dump.`,
79+
);
80+
}
81+
}
82+
}
83+
84+
mergeJayDump()
85+
.catch((e) => {
86+
console.error(e);
87+
})
88+
.finally(async () => {
89+
await prisma.$disconnect();
90+
});

collection/emails/output/.gitkeep

Whitespace-only changes.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Hi {{ name }},
2+
3+
You are yet to collect the following items that you bought from the DoCSoc summer merch drop last year:
4+
{% for item in itemsToCollect %}
5+
- {{ item.rootitem }} {{ item.variant }} x{{ item.quantity }} {% endfor %}
6+
7+
You can collect them at any of the following collection dates:
8+
- Monday 21st Oct 4:15-6pm, Huxley 315
9+
- Tuesday 22nd Oct 4:15-6pm, Huxley 315
10+
- Friday 25th Oct 2pm-3:45pm, Huxley 218
11+
- Monday 28th Oct 4:15pm-6pm, Huxley 315
12+
- Thursday 31st Oct, 3:15pm-4:45pm, Huxley 315
13+
- Monday 4th Nov, 4:15pm-6pm, Huxley 315
14+
- Friday 8th Nov, 2:30pm-5pm, Huxley 218
15+
16+
If you have already collected your merch, didn’t buy any merch or otherwise believe this email was sent incorrectly, please let us know!
17+
18+
Kind regards,
19+
DoCSoc Committee.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!-- DoCSoc Mail Merge Wrapper - used to wrap results rendered from Markdown -->
2+
<!-- You probably don't need to edit this file, but you can if you want to! -->
3+
<!DOCTYPE html>
4+
<html lang="en">
5+
<head>
6+
<meta charset="UTF-8">
7+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
8+
</head>
9+
<body>
10+
<!-- All HTML wrappers must provide a {{ content }} block to render the content fron the markdown-->
11+
{{ content }}
12+
</body>
13+
</html>

0 commit comments

Comments
 (0)