Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Raise CRM #347

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions apps/server/src/api/_router-imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
export const importMap = {
"/admin/audit-logs/by-object/{objectId}/get": () => import("./admin/audit-logs/by-object/{objectId}/get"),
"/admin/audit-logs/by-subject/{subjectId}/get": () => import("./admin/audit-logs/by-subject/{subjectId}/get"),
"/admin/campaigns/emails/get": () => import("./admin/campaigns/emails/get"),
"/admin/campaigns/emails/post": () => import("./admin/campaigns/emails/post"),
"/admin/campaigns/get": () => import("./admin/campaigns/get"),
"/admin/campaigns/post": () => import("./admin/campaigns/post"),
"/admin/fundraisers/get": () => import("./admin/fundraisers/get"),
"/admin/fundraisers/post": () => import("./admin/fundraisers/post"),
"/admin/fundraisers/{fundraiserId}/donations/get": () => import("./admin/fundraisers/{fundraiserId}/donations/get"),
Expand All @@ -30,6 +34,11 @@ export const importMap = {
"/admin/users/{userId}/patch": () => import("./admin/users/{userId}/patch"),
"/public/fundraisers/{fundraiserId}/donation/post": () => import("./public/fundraisers/{fundraiserId}/donation/post"),
"/public/fundraisers/{fundraiserId}/get": () => import("./public/fundraisers/{fundraiserId}/get"),
"/public/members/campaign/get": () => import("./public/members/campaign/get"),
"/public/members/campaign/post": () => import("./public/members/campaign/post"),
"/public/members/get": () => import("./public/members/get"),
"/public/members/subscribe/post": () => import("./public/members/subscribe/post"),
"/public/members/unsubscribe/post": () => import("./public/members/unsubscribe/post"),
"/public/status/get": () => import("./public/status/get"),
"/scheduler/collect-payments/post": () => import("./scheduler/collect-payments/post"),
"/stripe/webhook/post": () => import("./stripe/webhook/post"),
Expand Down
6 changes: 6 additions & 0 deletions apps/server/src/api/admin/campaigns/emails/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { scan } from '../../../../helpers/db';
import { emailTable } from '../../../../helpers/tables';
import { middyfy } from '../../../../helpers/wrapper';
import { $Emails } from '../../../../schemas';

export const main = middyfy(null, $Emails, true, async () => scan(emailTable));
38 changes: 38 additions & 0 deletions apps/server/src/api/admin/campaigns/emails/post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { fixedGroups } from '@raise/shared';
import { ulid } from 'ulid';
import { middyfy } from '../../../../helpers/wrapper';
import { assertHasGroup, insert, scan } from '../../../../helpers/db';
import { $EmailCreation, $Ulid } from '../../../../schemas';
import { sendEmail } from '../../../../helpers/email';
import {
campaignMemberTable, campaignTable, emailTable, memberTable
} from '../../../../helpers/tables';
import emailTemplate from '../../../../helpers/email/emailTemplate';

export const main = middyfy($EmailCreation, $Ulid, true, async (event) => {
assertHasGroup(event, fixedGroups.National);

const email = await insert(emailTable, {
id: ulid(),
time: Math.floor(new Date().getTime() / 1000),
...event.body,
message: emailTemplate(event.body).string
});

const campaignsFromDb = await scan(campaignTable);
const campaignMembersFromDb = await scan(campaignMemberTable);
const memberFromDb = await scan(memberTable);

const campaign = campaignsFromDb.find((c) => (c.id === event.body.recipient));
const campaignMembers = campaignMembersFromDb.filter((cm) => (cm.campaignId === campaign?.id) && (cm.active === true));
const campaignMembersEmails = memberFromDb.filter((m) => campaignMembers.find((cm) => (cm.memberId === m.id) && (cm.emailConsent === true)));

campaignMembersEmails.forEach(async (member) => (sendEmail(
event.body.subject,
emailTemplate(event.body),
member.email,
'[email protected]',
)));

return email.id;
});
6 changes: 6 additions & 0 deletions apps/server/src/api/admin/campaigns/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { scan } from '../../../helpers/db';
import { campaignTable } from '../../../helpers/tables';
import { middyfy } from '../../../helpers/wrapper';
import { $Campaigns } from '../../../schemas';

export const main = middyfy(null, $Campaigns, true, async () => scan(campaignTable));
24 changes: 24 additions & 0 deletions apps/server/src/api/admin/campaigns/post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { fixedGroups } from '@raise/shared';
import { ulid } from 'ulid';
import createHttpError from 'http-errors';
import { middyfy } from '../../../helpers/wrapper';
import { assertHasGroup, insert, scan } from '../../../helpers/db';
import { $CampaignCreation, $Ulid } from '../../../schemas';
import { campaignTable } from '../../../helpers/tables';

export const main = middyfy($CampaignCreation, $Ulid, true, async (event) => {
assertHasGroup(event, fixedGroups.National);

const campaignsFromDb = await scan(campaignTable);
if (campaignsFromDb.find((u) => (u.campaign === event.body.campaign) && (u.chapter === event.body.chapter))) {
throw new createHttpError.BadRequest('The campaign name has already been used for this chapter. Please pick a different name.');
}

const campaign = await insert(campaignTable, {
id: ulid(),
archived: false,
...event.body,
});

return campaign.id;
});
6 changes: 6 additions & 0 deletions apps/server/src/api/public/members/campaign/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { scan } from '../../../../helpers/db';
import { campaignMemberTable } from '../../../../helpers/tables';
import { middyfy } from '../../../../helpers/wrapper';
import { $CampaignMembers, } from '../../../../schemas';

export const main = middyfy(null, $CampaignMembers, true, async () => scan(campaignMemberTable));
53 changes: 53 additions & 0 deletions apps/server/src/api/public/members/campaign/post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { ulid } from 'ulid';
import createHttpError from 'http-errors';
import { middyfy } from '../../../../helpers/wrapper';
import { insert, scan, update } from '../../../../helpers/db';
import { $CampaignMemberCreation, $Ulid } from '../../../../schemas';
import { campaignMemberTable, memberTable } from '../../../../helpers/tables';

export const main = middyfy($CampaignMemberCreation, $Ulid, false, async (event) => {
if ((event.body.name === '') || (event.body.name === undefined)) throw new createHttpError.BadRequest('No Name Provided');
if ((event.body.email === '') || (event.body.email === undefined)) throw new createHttpError.BadRequest('No Email Provided');
if (event.body.email.toLowerCase().match(/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|.(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/) === (false || null)) throw new createHttpError.BadRequest('Invalid Email Format');

const usersFromDb = await scan(memberTable);
const campaignMembersFromDb = await scan(campaignMemberTable);

let member = usersFromDb.find((u) => u.email === event.body.email);

if (member) {
if (campaignMembersFromDb.find((cm) => (cm.memberId === member?.id) && (cm.campaignId === event.body.campaignId) && (cm.active === true))) {
throw new createHttpError.BadRequest('This email has already been used to sign up to this campaign');
} else if (campaignMembersFromDb.find((cm) => (cm.memberId === member?.id) && (cm.campaignId === event.body.campaignId) && (cm.active === false))) {
await update(campaignMemberTable, { memberId: member.id, campaignId: event.body.campaignId }, { active: true, emailConsent: event.body.emailConsent }); // re-signs-up member to the campaign (if they had previously unsubscribed)
if (member.name !== event.body.name) {
await update(memberTable, { id: member.id }, { name: event.body.name }); // updates the name should there be a change in name
}
} else {
await insert(campaignMemberTable, {
id: ulid(),
memberId: member.id,
campaignId: event.body.campaignId,
active: true,
emailConsent: event.body.emailConsent
});
}
} else {
member = await insert(memberTable, {
id: ulid(),
joined: Math.floor(new Date().getTime() / 1000),
active: true,
name: event.body.name,
email: event.body.email,
});
await insert(campaignMemberTable, {
id: ulid(),
memberId: member.id,
campaignId: event.body.campaignId,
active: true,
emailConsent: event.body.emailConsent
});
}

return member.id;
});
6 changes: 6 additions & 0 deletions apps/server/src/api/public/members/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { scan } from '../../../helpers/db';
import { memberTable } from '../../../helpers/tables';
import { middyfy } from '../../../helpers/wrapper';
import { $Members } from '../../../schemas';

export const main = middyfy(null, $Members, true, async () => scan(memberTable));
39 changes: 39 additions & 0 deletions apps/server/src/api/public/members/subscribe/post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { ulid } from 'ulid';
import createHttpError from 'http-errors';
import { middyfy } from '../../../../helpers/wrapper';
import { insert, scan } from '../../../../helpers/db';
import { $MemberCreation, $Ulid } from '../../../../schemas';
import { memberTable } from '../../../../helpers/tables';

export const main = middyfy($MemberCreation, $Ulid, false, async (event) => {
if ((event.body.name === '') || (event.body.name === undefined)) throw new createHttpError.BadRequest('No Name Provided');
if ((event.body.email === '') || (event.body.email === undefined)) throw new createHttpError.BadRequest('No Email Provided');
if (event.body.email.toLowerCase().match(/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|.(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/) === (false || null)) throw new createHttpError.BadRequest('Invalid Email Format');
// This is a general intrest list
const usersFromDb = await scan(memberTable);
if (usersFromDb.find((u) => u.email === event.body.email)) {
throw new createHttpError.BadRequest('Email already in use');
}

const member = await insert(memberTable, {
id: ulid(),
joined: Math.floor(new Date().getTime() / 1000),
active: true,
...event.body,
});

// (done) day/timestamp of sign up
// joinraise.org/bristol/signup?utm_campaign=Freshers fair 2023
// (done) unsubscribe page, enter email (Perhaps autofill joinraise.org/[email protected])
// (done) unsubscribe property for members
// auto clear form when success, maybe clear success message after a few seconds
// string array for chapters
// mini-segments within chapters???
// joinraise.org./signup/{campaign} (Exmaple RSVP form)
// sign up to Mailchimp and what kind of list (subset of a table) and campaigns.
// Subscribers Table (name, email, day of signup, opt out, id), kinda what the member list is right now
// then another list table for each chapter, (Summer party, Raise bristol 2022, Raise Bristol 2023),
// then another list that say which person is on which list.

return member.id;
});
21 changes: 21 additions & 0 deletions apps/server/src/api/public/members/unsubscribe/post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import createHttpError from 'http-errors';
import { middyfy } from '../../../../helpers/wrapper';
import {
scan, update
} from '../../../../helpers/db';
import { $CampaignMemberRemoval, $Ulid } from '../../../../schemas';
import { campaignMemberTable, memberTable } from '../../../../helpers/tables';

export const main = middyfy($CampaignMemberRemoval, $Ulid, false, async (event) => {
if ((event.body.email === '') || (event.body.email === undefined)) throw new createHttpError.BadRequest('No Email Provided');
if (event.body.email.toLowerCase().match(/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|.(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/) === (false || null)) throw new createHttpError.BadRequest('Invalid Email Format');

const usersFromDb = await scan(memberTable);
const user = usersFromDb.find((u) => (u.email === event.body.email));
if (user) {
await update(campaignMemberTable, { memberId: user.id, campaignId: event.body.campaignId }, { active: false }); // Fix campaign Id in table issue
} else {
throw new createHttpError.BadRequest('Account could not be found. Please check your details');
}
return user.id;
});
5 changes: 3 additions & 2 deletions apps/server/src/helpers/email/confirmation.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { makeDonation, makeFundraiser, makePayment } from '../../../local/testHelpers';
import confirmation from './confirmation';
import footer from './footer';
import renderFooter from './footerTemplate';
import renderHtml from './renderHtml';

test('renders email correctly with one payment', () => {
// given fundraiser, donation and payments
Expand Down Expand Up @@ -192,7 +193,7 @@ test('does not confuse MWA and Raise branding', () => {
const email = confirmation(fundraiser, donation, payments).string.replace(/\s+/g, ' ');

// renders footer so we can avoid it from the checks
const pageEnd = footer().string.replace(/\s+/g, ' ');
const pageEnd = renderHtml`${renderFooter()}`.string.replace(/\s+/g, ' ');

// then we expect the email not to mention raise except for the image assets
expect(email
Expand Down
4 changes: 2 additions & 2 deletions apps/server/src/helpers/email/confirmation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
import env from '../../env/env';
import { Donation, Fundraiser, Payment } from '../../schemas';
import renderHtml, { RenderedHtml } from './renderHtml';
import footer from './footer';
import renderFooter from './footerTemplate';

export default (fundraiser: Fundraiser, donation: Donation, payments: Payment[]): RenderedHtml => {
const totalDonated = payments.reduce((acc, p) => acc + p.donationAmount, 0);
Expand Down Expand Up @@ -346,7 +346,7 @@ export default (fundraiser: Fundraiser, donation: Donation, payments: Payment[])
</tbody>
</table>
</div>
${footer()}
${renderFooter()}
</div>
</body>

Expand Down
Loading
Loading