Skip to content

Commit

Permalink
feat: add reminder emails for collection system
Browse files Browse the repository at this point in the history
  • Loading branch information
Gum-Joe committed Oct 21, 2024
1 parent 0ceb6dc commit 0b8eee9
Show file tree
Hide file tree
Showing 12 changed files with 313 additions and 0 deletions.
13 changes: 13 additions & 0 deletions collection/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,16 @@ Other tools have their own package.jsons as that was how they were originally se
# Docker

To build the docker image, run `npx nx container collection` from the _root_ of the repo.

# What `emails/` for?

This is for sending reminder emails to those who haven't collected their merch yet.

Usage:

```bash
cd emails
npx tsx getUncollectedPeople.ts # outputs a JSON file to output/reminders.json
docsoc-mailmerge generate nunjucks ./data/reminders.json ./templates/reminder.njk -s json -o output -n reminders
# then send (check README in emails folder)!
```
18 changes: 18 additions & 0 deletions collection/emails/.env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Fill these in to send emails
DOCSOC_SMTP_SERVER=smtp-mail.outlook.com
DOCSOC_SMTP_PORT=587
DOCSOC_OUTLOOK_USERNAME=
# Password to docsoc email
DOCSOC_OUTLOOK_PASSWORD=

# Optional: Fill these in to uplod drafts
# You will need to create an app registration in Entra ID, restricted to the organisation,
# And grant it the following permissions:
# - Mail.ReadWrite
# - User.Read
MS_ENTRA_CLIENT_ID=
MS_ENTRA_CLIENT_SECRET=
MS_ENTRA_TENANT_ID=

# DB URL for use by getUncollectedPeople
COLLECTION_DATABASE_URL=
9 changes: 9 additions & 0 deletions collection/emails/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
attachments/*
!attachments/.gitkeep

data/*
!data/.gitkeep
!data/example.csv

output/*
!output/.gitkeep
59 changes: 59 additions & 0 deletions collection/emails/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
To get started:

1. Put your own CSV in the `data` folder, with at the minimum `to` and `subject` columns.
1. You can also add `cc` and `bcc` columns (to use them you will need to pass the correct CLI option though)
2. `to`, `cc`, and `bcc` can be a space-separated list of emails.
3. You can add any other columns you like, and they will be available in the template.
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`).
1. Or, pass the same attachment to every email using the `-a` flag to `generate`
5. For multiple attachments, have separate columns e.g. `attachment1`, `attachment2`, etc.
6. See `data/example.csv` for an example.
2. Put your own nunjucks markdown email template in the `templates` folder.
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.
3. Fill in the `.env` file with your email credentials.

Then run the following commands:

```bash
docsoc-mailmerge generate nunjucks ./data/my-data.csv ./templates/my-template.md.njk -o ./output --htmlTemplate ./templates/wrapper.html.njk
# make some edits to the outputs and regenerate them:
docsoc-mailmerge regenerate ./output/<runname>
# review them, then send:
docsoc-mailmerge send ./output/<runname>
```

The CLI tool has many options - use `--help` to see them all:

```bash
docsoc-mailmerge generate nunjucks --help
docsoc-mailmerge regenerate --help
docsoc-mailmerge send --help
```

## What happen when you generate

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
2. The HTML files, which is what is actually sent, can be regenerated after edting the markdown files with `regenerate` command (see below)
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.

## If the .env file is missing

Use this template:

```bash
# Fill these in to send emails
DOCSOC_SMTP_SERVER=smtp-mail.outlook.com
DOCSOC_SMTP_PORT=587
DOCSOC_OUTLOOK_USERNAME=
# Password to docsoc email
DOCSOC_OUTLOOK_PASSWORD=

# Optional: Fill these in to uplod drafts
# You will need to create an app registration in Entra ID, restricted to the organisation,
# And grant it the following permissions:
# - Mail.ReadWrite
# - User.Read
MS_ENTRA_CLIENT_ID=
MS_ENTRA_CLIENT_SECRET=
MS_ENTRA_TENANT_ID=
```
Empty file.
Empty file added collection/emails/data/.gitkeep
Empty file.
2 changes: 2 additions & 0 deletions collection/emails/data/example.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
name,to,subject,attachment1,attachment2,bcc,cc
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
90 changes: 90 additions & 0 deletions collection/emails/getUncollectedPeople.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* Outputs a JSON file with the students who have not collected their merch, for use by the mailmerge tool
* to send a reminder to these people.
*
* Created with co-pilot.
*/
import { PrismaClient } from "@prisma/client";
// Load .env file ".env"
import dotenv from "dotenv";
import fs from "fs/promises";

dotenv.config();

const prisma = new PrismaClient();

interface OutputRecord {
to: string;
name: string;
shortcode: string;
itemsToCollect: {
rootitem: string;
variant: string;
quantity: number;
}[];
subject: string;
}

async function getUncollectedPeople() {
const uncollectedOrders = await prisma.orderItem.findMany({
where: { collected: false },
include: {
Order: {
include: {
ImperialStudent: true,
},
},
Variant: {
include: {
RootItem: true,
},
},
},
});

const outputRecords: OutputRecord[] = [];

const studentMap = new Map<string, OutputRecord>();

for (const orderItem of uncollectedOrders) {
const student = orderItem.Order.ImperialStudent;
const studentKey = student.email;

// Filter out if rootitem is "Pride Lanyard"
// (in 2024 we started handing out these lanyard to whoever after the summer collection so
/// it was likely that anyway who didn't collect it during merch collections got them during the random hand outs we did)
if (orderItem.Variant.RootItem.name === "Pride Lanyard") {
console.log(`Skipping ${studentKey} for Pride Lanyard`);
continue;
}

if (!studentMap.has(studentKey)) {
studentMap.set(studentKey, {
to: student.email,
name: `${student.firstName} ${student.lastName}`,
shortcode: student.shortcode,
itemsToCollect: [],
subject: "Collect your remaining DoCSoc Summer Merch",
});
}

const studentRecord = studentMap.get(studentKey)!;
studentRecord.itemsToCollect.push({
rootitem: orderItem.Variant.RootItem.name,
variant: orderItem.Variant.variantName,
quantity: orderItem.quantity,
});
}

outputRecords.push(...studentMap.values());

await fs.writeFile("data/reminders.json", JSON.stringify(outputRecords, null, 2));
}

getUncollectedPeople()
.catch((e) => {
console.error(e);
})
.finally(async () => {
await prisma.$disconnect();
});
90 changes: 90 additions & 0 deletions collection/emails/mergeJay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* You can safely ignore this file. It is used to merge the data from Jay's dump with the local database.
*
* 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.
*/
import { PrismaClient } from "@prisma/client";
import { parse } from "csv-parse";
import dotenv from "dotenv";
import fs from "fs/promises";

dotenv.config();

const prisma = new PrismaClient();

async function getStudentShortcode(orderItemId: number) {
const orderItem = await prisma.orderItem.findUnique({
where: { id: orderItemId },
include: {
Order: {
include: {
ImperialStudent: true,
},
},
},
});

if (!orderItem) {
throw new Error(`OrderItem with ID ${orderItemId} does not exist.`);
}

const studentShortcode = orderItem.Order.ImperialStudent.shortcode;
return studentShortcode;
}

async function mergeJayDump() {
// Load Jay's dump
const jayDumpContent = await fs.readFile("data/jay-dump.csv", "utf-8");
const jayDumpRecords = await parse(jayDumpContent, {
columns: true,
delimiter: "\t",
});

for await (const record of jayDumpRecords) {
const orderItemId = parseInt(record.id, 10);
const jayCollected = record.collected === "t";

// Check if the order item exists in the local database
const localOrderItem = await prisma.orderItem.findUnique({
where: { id: orderItemId },
});

if (!localOrderItem) {
// throw new Error(
// `OrderItem with ID ${orderItemId} does not exist in the local database.`,
// );
console.error(`OrderItem with ID ${orderItemId} does not exist in the local database!`);
continue;
}

if (jayCollected && !localOrderItem.collected) {
// Update the local database to mark the item as collected
// await prisma.orderItem.update({
// where: { id: orderItemId },
// data: { collected: true },
// });
console.warn(
`JAY TRUE, KISHAN FALSE: OrderItem with ID ${orderItemId} marked as collected in the local database. Shortcode: ${await getStudentShortcode(
orderItemId,
)}`,
);

// Print shortcode of the student
}

// if I have it marked as true, but Jay has it marked as false, report this
if (localOrderItem.collected && !jayCollected) {
console.warn(
`KISHAN TRUE, JAY FALSE: OrderItem with ID ${orderItemId} marked as collected in the local database, but not in Jay's dump.`,
);
}
}
}

mergeJayDump()
.catch((e) => {
console.error(e);
})
.finally(async () => {
await prisma.$disconnect();
});
Empty file.
19 changes: 19 additions & 0 deletions collection/emails/templates/reminder.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Hi {{ name }},

You are yet to collect the following items that you bought from the DoCSoc summer merch drop last year:
{% for item in itemsToCollect %}
- {{ item.rootitem }} {{ item.variant }} x{{ item.quantity }} {% endfor %}

You can collect them at any of the following collection dates:
- Monday 21st Oct 4:15-6pm, Huxley 315
- Tuesday 22nd Oct 4:15-6pm, Huxley 315
- Friday 25th Oct 2pm-3:45pm, Huxley 218
- Monday 28th Oct 4:15pm-6pm, Huxley 315
- Thursday 31st Oct, 3:15pm-4:45pm, Huxley 315
- Monday 4th Nov, 4:15pm-6pm, Huxley 315
- Friday 8th Nov, 2:30pm-5pm, Huxley 218

If you have already collected your merch, didn’t buy any merch or otherwise believe this email was sent incorrectly, please let us know!

Kind regards,
DoCSoc Committee.
13 changes: 13 additions & 0 deletions collection/emails/templates/wrapper.html.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- DoCSoc Mail Merge Wrapper - used to wrap results rendered from Markdown -->
<!-- You probably don't need to edit this file, but you can if you want to! -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<!-- All HTML wrappers must provide a {{ content }} block to render the content fron the markdown-->
{{ content }}
</body>
</html>

0 comments on commit 0b8eee9

Please sign in to comment.