-
-
Notifications
You must be signed in to change notification settings - Fork 33
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: setup the email notification for VAT
- Loading branch information
Showing
4 changed files
with
179 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; | ||
|
@@ -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 { | ||
|
@@ -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 { | ||
|
@@ -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 = () => { | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
|
||
|
@@ -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' | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |