Skip to content

Commit

Permalink
chore: setup the email notification for VAT
Browse files Browse the repository at this point in the history
  • Loading branch information
aalemayhu committed Nov 26, 2024
1 parent 6336287 commit c60239b
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 0 deletions.
49 changes: 49 additions & 0 deletions src/lib/storage/jobs/helpers/sendVatNotificationEmail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useDefaultEmailService } from '../../../../services/EmailService/EmailService';
import { getStripe } from '../../../integrations/stripe';
import Stripe from 'stripe';

const stripe = getStripe();

const sendVatNotificationEmail = async () => {
const emailService = useDefaultEmailService();
let hasMore = true;
let startingAfter: string | undefined = undefined;

while (hasMore) {
const subscriptions: Stripe.ApiList<Stripe.Subscription> =
await stripe.subscriptions.list({
limit: 100,
status: 'active',
starting_after: startingAfter,
});

for (const subscription of subscriptions.data) {
if (typeof subscription.customer === 'string') {
const customer = await stripe.customers.retrieve(subscription.customer);

if ('email' in customer) {
console.log({
email: customer.email,
currency: subscription.currency,
});
await emailService.sendVatNotificationEmail(
customer.email ?? `alexander+${subscription.id}@alemayhu.com`, // fallback to inform dev if email is not set
subscription.currency,
customer.name ?? 'there'
);
} else {
console.warn('Customer does not have an email');
}
}
}

hasMore = subscriptions.has_more;
if (hasMore) {
startingAfter = subscriptions.data[subscriptions.data.length - 1].id;
}
}
};

if (require.main === module) {
sendVatNotificationEmail().catch(console.error);
}
90 changes: 90 additions & 0 deletions src/services/EmailService/EmailService.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import sgMail = require('@sendgrid/mail');
import * as cheerio from 'cheerio';
import * as fs from 'fs';
import * as path from 'path';

import {
CONVERT_LINK_TEMPLATE,
CONVERT_TEMPLATE,
DEFAULT_SENDER,
PASSWORD_RESET_TEMPLATE,
VAT_NOTIFICATION_TEMPLATE,
VAT_NOTIFICATIONS_LOG_PATH,
} from './constants';
import { isValidDeckName, addDeckNameSuffix } from '../../lib/anki/format';
import { ClientResponse } from '@sendgrid/mail';
Expand All @@ -22,6 +27,11 @@ export interface IEmailService {
message: string,
attachments: Express.Multer.File[]
): Promise<EmailResponse>;
sendVatNotificationEmail(
email: string,
currency: string,
name: string
): Promise<void>;
}

class EmailService implements IEmailService {
Expand Down Expand Up @@ -120,6 +130,77 @@ class EmailService implements IEmailService {
return { didSend: false, error: e as Error };
}
}

private loadVatNotificationsSent(): Set<string> {
try {
// Ensure .2anki directory exists
const dir = path.dirname(VAT_NOTIFICATIONS_LOG_PATH);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}

if (fs.existsSync(VAT_NOTIFICATIONS_LOG_PATH)) {
const data = fs.readFileSync(VAT_NOTIFICATIONS_LOG_PATH, 'utf8');
return new Set(JSON.parse(data));
}
} catch (error) {
console.warn('Error loading VAT notifications log:', error);
}
return new Set();
}

private saveVatNotificationSent(email: string): void {
try {
const vatNotificationsSent = this.loadVatNotificationsSent();
vatNotificationsSent.add(email);
fs.writeFileSync(
VAT_NOTIFICATIONS_LOG_PATH,
JSON.stringify([...vatNotificationsSent])
);
} catch (error) {
console.error('Error saving VAT notification log:', error);
}
}

async sendVatNotificationEmail(
email: string,
currency: string,
name: string
): Promise<void> {
const vatNotificationsSent = this.loadVatNotificationsSent();
if (vatNotificationsSent.has(email)) {
console.log(`Skipping ${email} - VAT notification already sent`);
return;
}

const amount = currency === 'eur' ? '€2' : '$2';
const markup = VAT_NOTIFICATION_TEMPLATE.replace(
'{{amount}}',
amount
).replace('{{name}}', name || 'there');

// Convert HTML to text using cheerio
const $ = cheerio.load(markup);
const text = $('body').text().replace(/\s+/g, ' ').trim();

const msg = {
to: email,
from: this.defaultSender,
subject: '2anki.net - Upcoming Changes to VAT',
text,
html: markup,
replyTo: '[email protected]',
};

try {
await sgMail.send(msg);
this.saveVatNotificationSent(email);
console.log(`Successfully sent VAT notification to ${email}`);
} catch (error) {
console.error(`Failed to send VAT notification to ${email}:`, error);
throw error;
}
}
}

export class UnimplementedEmailService implements IEmailService {
Expand Down Expand Up @@ -150,6 +231,15 @@ export class UnimplementedEmailService implements IEmailService {
);
return Promise.resolve({ didSend: false });
}

sendVatNotificationEmail(
email: string,
currency: string,
name: string
): Promise<void> {
console.info('sendVatNotificationEmail not handled', email, currency, name);
return Promise.resolve();
}
}

export const useDefaultEmailService = () => {
Expand Down
12 changes: 12 additions & 0 deletions src/services/EmailService/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import path from 'path';
import fs from 'fs';
import * as os from 'os';

export const EMAIL_TEMPLATES_DIRECTORY = path.join(__dirname, 'templates');

Expand All @@ -19,3 +20,14 @@ export const CONVERT_LINK_TEMPLATE = fs.readFileSync(
);

export const DEFAULT_SENDER = '2anki.net <[email protected]>';

export const VAT_NOTIFICATION_TEMPLATE = fs.readFileSync(
path.join(EMAIL_TEMPLATES_DIRECTORY, 'vat-notification.html'),
'utf8'
);

export const VAT_NOTIFICATIONS_LOG_PATH = path.join(
os.homedir(),
'.2anki',
'vat-notifications-sent.json'
);
28 changes: 28 additions & 0 deletions src/services/EmailService/templates/vat-notification.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<p>Hei {{name}},</p>

<p>
We want to inform you about an upcoming change to your 2anki subscription. As a Norwegian company (Lær Smart
AS),
we are required by law to collect Value Added Tax (VAT) on our services.
</p>

<p>
<strong>What this means for your subscription</strong><br>
Your current monthly subscription costs {{amount}}. In the near future,
we will need to add 25% VAT to this amount, as required by Norwegian law.
</p>

<p>
We understand this change might affect your decision to continue using our service. If you wish to cancel your
subscription,
you can do so at any time using <a href="https://buy.stripe.com/cN2cPC6ek7RCbjGdQU">this link</a>.
If you have any questions or concerns, please email us at
<a href="mailto:[email protected]">[email protected]</a>.
</p>

<p style="margin-top: 2em; color: #666;">
--<br>
Happy learning,<br>
The 2anki Team<br>
<a href="https://2anki.net/" style="color: #0066cc; text-decoration: none;">2anki.net</a>
</p>

0 comments on commit c60239b

Please sign in to comment.