Browse Source

feat(scheduler): add scheduled emails contents

Karol Sójko 2 năm trước cách đây
mục cha
commit
6e0855f9b3

+ 1 - 0
.pnp.cjs

@@ -3056,6 +3056,7 @@ const RAW_RUNTIME_STATE =
           ["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
           ["@sentry/node", "npm:7.19.0"],\
           ["@standardnotes/common", "workspace:packages/common"],\
+          ["@standardnotes/domain-core", "workspace:packages/domain-core"],\
           ["@standardnotes/domain-events", "workspace:packages/domain-events"],\
           ["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
           ["@standardnotes/predicates", "workspace:packages/predicates"],\

+ 1 - 0
packages/scheduler/jest.config.js

@@ -7,4 +7,5 @@ module.exports = {
   transform: {
     ...tsjPreset.transform,
   },
+  coveragePathIgnorePatterns: ['/Bootstrap/', '/Infra/', '/Domain/Email/', '/Domain/Event/'],
 }

+ 1 - 0
packages/scheduler/package.json

@@ -27,6 +27,7 @@
     "@newrelic/winston-enricher": "^4.0.0",
     "@sentry/node": "^7.19.0",
     "@standardnotes/common": "workspace:*",
+    "@standardnotes/domain-core": "workspace:^",
     "@standardnotes/domain-events": "workspace:*",
     "@standardnotes/domain-events-infra": "workspace:*",
     "@standardnotes/predicates": "workspace:*",

+ 9 - 0
packages/scheduler/src/Domain/Email/EncourageEmailBackups.ts

@@ -0,0 +1,9 @@
+import { readFileSync } from 'fs'
+
+export function getSubject(): string {
+  return 'Enable email backups for your account'
+}
+
+export function getBody(): string {
+  return readFileSync(`${__dirname}/encourage-email-backups.html`).toString()
+}

+ 11 - 0
packages/scheduler/src/Domain/Email/EncourageSubscriptionPurchasing.ts

@@ -0,0 +1,11 @@
+import { readFileSync } from 'fs'
+
+export function getSubject(): string {
+  return 'Checking in after one month with Standard Notes'
+}
+
+export function getBody(registrationDate: string): string {
+  const body = readFileSync(`${__dirname}/encourage-subscription-purchasing.html`).toString()
+
+  return body.replace('%%REGISTRATION_DATE%%', registrationDate)
+}

+ 9 - 0
packages/scheduler/src/Domain/Email/ExitInterview.ts

@@ -0,0 +1,9 @@
+import { readFileSync } from 'fs'
+
+export function getSubject(): string {
+  return 'Can we ask why you canceled?'
+}
+
+export function getBody(): string {
+  return readFileSync(`${__dirname}/exit-interview.html`).toString()
+}

+ 18 - 0
packages/scheduler/src/Domain/Email/encourage-email-backups.html

@@ -0,0 +1,18 @@
+<div>
+  <p>
+    Did you know you can enable daily email backups for your account? This <strong>free</strong> feature sends an
+    email to your inbox with an encrypted backup file including all your notes and tags.
+  </p>
+  <p>
+    Email backups are an important feature that help protect you against worst-case scenarios. Your backups can be
+    used to restore your account to a previous state, or to import old versions of notes into your present
+    account.
+  </p>
+  <p>
+    To enable free email backups, use the Standard Notes web or desktop app, and open Preferences > Backups > Email Backups.
+  </p>
+
+  <a href="https://standardnotes.com/help/28/how-do-i-enable-daily-email-backups">
+    Learn more about daily email backups →
+  </a>
+</div>

+ 83 - 0
packages/scheduler/src/Domain/Email/encourage-subscription-purchasing.html

@@ -0,0 +1,83 @@
+<div>
+  <p>Hi there,</p>
+  <p>
+    We hope you've been finding great use out of Standard Notes. We built Standard Notes to be a secure place for
+    your most sensitive notes and files.
+  </p>
+  <p>
+    As a reminder,
+    <strong>
+      <em>you signed up for the Standard Notes free plan on %%REGISTRATION_DATE%%</em>
+    </strong>
+    Your free account comes with standard features like end-to-end encryption, multiple-device sync, and
+    two-factor authentication.
+  </p>
+  <p>
+    If you're ready to advance your usage of Standard Notes, we recommend upgrading to one of our more powerful
+    plans.
+  </p>
+  <ul>
+    <li>
+      <p>
+        <strong>Productivity</strong> <strong>($59/year)</strong> powers up your editing experience with unique
+        and special-built note-types for markdown, rich text, spreadsheets, todo, and more.
+      </p>
+    </li>
+    <li>
+      <p>
+        <strong>Professional</strong> <strong>($99/year)</strong> gives you all the power of Productivity plus
+        100GB of end-to-end encrypted file storage for your private photos, videos, and documents, plus family
+        subscription sharing with up to 5 people.
+      </p>
+    </li>
+  </ul>
+  <p>
+    Professional comes with a 90-day money back guarantee, so if you're not completely satisfied, we're happy to
+    refund your full purchase amount.
+  </p>
+  <p>
+    <strong>
+      <a href="https://standardnotes.com/plans">Upgrade your plan →</a>
+    </strong>
+  </p>
+  <p>
+    <strong>
+      <a href="https://standardnotes.com/features">Learn more about the features →</a>
+    </strong>
+  </p>
+  <p>
+    <strong>Questions & Answers</strong>
+  </p>
+  <p>
+    <em>How does Standard Notes compare with conventional note-taking apps?</em>
+  </p>
+  <p>
+    Data you store with Standard Notes is encrypted with end-to-end encryption using a key only you know. Because
+    of this, we can't read your notes, and neither can anyone else.
+  </p>
+  <p>
+    <em>What kind of notes should I store in Standard Notes?</em>
+  </p>
+  <p>
+    This question can be reframed as: "What shouldn't I store in non-private services?" This would include
+    sensitive/sensual data related to your health and wellness, secrets and keys, notes and documents with
+    personally identifiable information that, if leaked, would lead to the theft of your identity, and business,
+    financial, or legal information which cover non-public or confidential information.
+  </p>
+  <p>
+    <em>Where can I access my notes?</em>
+  </p>
+  <p>
+    Providing you with easy access to your notes and files on all your devices is a key focus for us. We provide
+    secure and well-designed applications for your web browser, desktop (macOS, Windows, Linux,) and mobile
+    (Android and iOS).
+  </p>
+  <p>
+    <em>I have more questions.</em>
+  </p>
+  <p>
+    We love questions. Head over to our Help page to see if your question is answered there. If not, reply
+    directly to this email or send an email to <a href="help@standardnotes.com">help@standardnotes.com</a> and
+    we'd be happy to help.
+  </p>
+</div>,

+ 28 - 0
packages/scheduler/src/Domain/Email/exit-interview.html

@@ -0,0 +1,28 @@
+<div>
+  <p>
+    We're truly sad to see you leave. Our mission is simple: build the best, most private, and most secure
+    note-taking app available. It's clear we've fallen short of your expectations somewhere along the way.
+  </p>
+  <p>
+    We just want you to know—if price was the reason you canceled, we're not willing to lose you. That's no issue
+    for us and we're happy to work out something that fits better with your budget. If price is your primary
+    concern, please click the link below, and we'll get in touch with some options.
+  </p>
+  <a href="https://app.standardnotes.com/?user-request=exit-discount">Apply For A Limited Discount Offer →</a>
+  <p>
+    If you canceled for another reason, such as a missing feature, or a feature that wasn't behaving or working as
+    you expected, please let us know! We build this product for you, and feedback from customers like yourself who
+    are willing to pay for a product is most crucial for us as we continue to evolve and iterate on Standard
+    Notes.
+  </p>
+  <p>If you have a minute, please fill out this brief exit interview: </p>
+  <a href="https://standardnotes.typeform.com/to/dX5lzPtm">Short Exit Interview →</a>
+  <p>
+    Our team reads every single response, and your feedback will be shared with the relevant department within our
+    team.
+  </p>
+  <p>
+    If you have any other thoughts or questions, please feel free to reply directly to this email, and a member of
+    our support team will be in touch with you.
+  </p>
+</div>

+ 0 - 223
packages/scheduler/src/Domain/Event/DomainEventFactory.spec.ts

@@ -1,223 +0,0 @@
-import 'reflect-metadata'
-
-import { EmailMessageIdentifier } from '@standardnotes/common'
-import { TimerInterface } from '@standardnotes/time'
-
-import { DomainEventFactory } from './DomainEventFactory'
-import { PredicateAuthority, PredicateName } from '@standardnotes/predicates'
-import { Job } from '../Job/Job'
-import { Predicate } from '../Predicate/Predicate'
-
-describe('DomainEventFactory', () => {
-  let timer: TimerInterface
-
-  const createFactory = () => new DomainEventFactory(timer)
-
-  beforeEach(() => {
-    timer = {} as jest.Mocked<TimerInterface>
-    timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(1)
-    timer.getUTCDate = jest.fn().mockReturnValue(new Date(1))
-  })
-
-  it('should create a DISCOUNT_APPLY_REQUESTED event', () => {
-    expect(
-      createFactory().createDiscountApplyRequestedEvent({
-        userEmail: 'test@test.te',
-        discountCode: 'off-10',
-      }),
-    ).toEqual({
-      createdAt: expect.any(Date),
-      meta: {
-        correlation: {
-          userIdentifier: 'test@test.te',
-          userIdentifierType: 'email',
-        },
-        origin: 'scheduler',
-      },
-      payload: {
-        userEmail: 'test@test.te',
-        discountCode: 'off-10',
-      },
-      type: 'DISCOUNT_APPLY_REQUESTED',
-    })
-  })
-
-  it('should create a DISCOUNT_WITHDRAW_REQUESTED event', () => {
-    expect(
-      createFactory().createDiscountWithdrawRequestedEvent({
-        userEmail: 'test@test.te',
-        discountCode: 'off-10',
-      }),
-    ).toEqual({
-      createdAt: expect.any(Date),
-      meta: {
-        correlation: {
-          userIdentifier: 'test@test.te',
-          userIdentifierType: 'email',
-        },
-        origin: 'scheduler',
-      },
-      payload: {
-        userEmail: 'test@test.te',
-        discountCode: 'off-10',
-      },
-      type: 'DISCOUNT_WITHDRAW_REQUESTED',
-    })
-  })
-
-  it('should create a EXIT_DISCOUNT_WITHDRAW_REQUESTED event', () => {
-    expect(
-      createFactory().createExitDiscountWithdrawRequestedEvent({
-        userEmail: 'test@test.te',
-        discountCode: 'exit-20',
-      }),
-    ).toEqual({
-      createdAt: expect.any(Date),
-      meta: {
-        correlation: {
-          userIdentifier: 'test@test.te',
-          userIdentifierType: 'email',
-        },
-        origin: 'scheduler',
-      },
-      payload: {
-        userEmail: 'test@test.te',
-        discountCode: 'exit-20',
-      },
-      type: 'EXIT_DISCOUNT_WITHDRAW_REQUESTED',
-    })
-  })
-
-  it('should create a EMAIL_MESSAGE_REQUESTED event', () => {
-    expect(
-      createFactory().createEmailMessageRequestedEvent({
-        userEmail: 'test@test.te',
-        messageIdentifier: EmailMessageIdentifier.ENCOURAGE_EMAIL_BACKUPS,
-        context: {
-          foo: 'bar',
-        },
-      }),
-    ).toEqual({
-      createdAt: expect.any(Date),
-      meta: {
-        correlation: {
-          userIdentifier: 'test@test.te',
-          userIdentifierType: 'email',
-        },
-        origin: 'scheduler',
-      },
-      payload: {
-        messageIdentifier: 'ENCOURAGE_EMAIL_BACKUPS',
-        userEmail: 'test@test.te',
-        context: {
-          foo: 'bar',
-        },
-      },
-      type: 'EMAIL_MESSAGE_REQUESTED',
-    })
-  })
-
-  it('should create a PREDICATE_VERIFICATION_REQUESTED event dedicated for auth', () => {
-    expect(
-      createFactory().createPredicateVerificationRequestedEvent(
-        {
-          uuid: '1-2-3',
-          userIdentifier: '2-3-4',
-          userIdentifierType: 'uuid',
-        } as jest.Mocked<Job>,
-        {
-          authority: PredicateAuthority.Auth,
-          name: PredicateName.EmailBackupsEnabled,
-          status: 'pending',
-        } as jest.Mocked<Predicate>,
-      ),
-    ).toEqual({
-      createdAt: expect.any(Date),
-      meta: {
-        correlation: {
-          userIdentifier: '2-3-4',
-          userIdentifierType: 'uuid',
-        },
-        origin: 'scheduler',
-        target: 'auth',
-      },
-      payload: {
-        predicate: {
-          authority: 'auth',
-          jobUuid: '1-2-3',
-          name: 'email-backups-enabled',
-        },
-      },
-      type: 'PREDICATE_VERIFICATION_REQUESTED',
-    })
-  })
-
-  it('should create a PREDICATE_VERIFICATION_REQUESTED event dedicated for syncing server', () => {
-    expect(
-      createFactory().createPredicateVerificationRequestedEvent(
-        {
-          uuid: '1-2-3',
-          userIdentifier: '2-3-4',
-          userIdentifierType: 'uuid',
-        } as jest.Mocked<Job>,
-        {
-          authority: PredicateAuthority.SyncingServer,
-          name: PredicateName.EmailBackupsEnabled,
-          status: 'pending',
-        } as jest.Mocked<Predicate>,
-      ),
-    ).toEqual({
-      createdAt: expect.any(Date),
-      meta: {
-        correlation: {
-          userIdentifier: '2-3-4',
-          userIdentifierType: 'uuid',
-        },
-        origin: 'scheduler',
-        target: 'syncing-server',
-      },
-      payload: {
-        predicate: {
-          authority: 'syncing-server',
-          jobUuid: '1-2-3',
-          name: 'email-backups-enabled',
-        },
-      },
-      type: 'PREDICATE_VERIFICATION_REQUESTED',
-    })
-  })
-
-  it('should create a PREDICATE_VERIFICATION_REQUESTED event dedicated for unknown target', () => {
-    expect(
-      createFactory().createPredicateVerificationRequestedEvent(
-        {
-          uuid: '1-2-3',
-          userIdentifier: '2-3-4',
-          userIdentifierType: 'uuid',
-        } as jest.Mocked<Job>,
-        {
-          authority: 'foobar' as PredicateAuthority,
-          name: PredicateName.EmailBackupsEnabled,
-          status: 'pending',
-        } as jest.Mocked<Predicate>,
-      ),
-    ).toEqual({
-      createdAt: expect.any(Date),
-      meta: {
-        correlation: {
-          userIdentifier: '2-3-4',
-          userIdentifierType: 'uuid',
-        },
-        origin: 'scheduler',
-      },
-      payload: {
-        predicate: {
-          authority: 'foobar',
-          jobUuid: '1-2-3',
-          name: 'email-backups-enabled',
-        },
-      },
-      type: 'PREDICATE_VERIFICATION_REQUESTED',
-    })
-  })
-})

+ 8 - 7
packages/scheduler/src/Domain/Event/DomainEventFactory.ts

@@ -1,9 +1,8 @@
-import { EmailMessageIdentifier } from '@standardnotes/common'
 import {
   DiscountApplyRequestedEvent,
   DiscountWithdrawRequestedEvent,
   DomainEventService,
-  EmailMessageRequestedEvent,
+  EmailRequestedEvent,
   ExitDiscountWithdrawRequestedEvent,
   PredicateVerificationRequestedEvent,
 } from '@standardnotes/domain-events'
@@ -70,13 +69,15 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
     }
   }
 
-  createEmailMessageRequestedEvent(dto: {
+  createEmailRequestedEvent(dto: {
     userEmail: string
-    messageIdentifier: EmailMessageIdentifier
-    context: Record<string, unknown>
-  }): EmailMessageRequestedEvent {
+    messageIdentifier: string
+    level: string
+    body: string
+    subject: string
+  }): EmailRequestedEvent {
     return {
-      type: 'EMAIL_MESSAGE_REQUESTED',
+      type: 'EMAIL_REQUESTED',
       createdAt: this.timer.getUTCDate(),
       meta: {
         correlation: {

+ 7 - 6
packages/scheduler/src/Domain/Event/DomainEventFactoryInterface.ts

@@ -1,8 +1,7 @@
-import { EmailMessageIdentifier } from '@standardnotes/common'
 import {
   DiscountApplyRequestedEvent,
   DiscountWithdrawRequestedEvent,
-  EmailMessageRequestedEvent,
+  EmailRequestedEvent,
   ExitDiscountWithdrawRequestedEvent,
   PredicateVerificationRequestedEvent,
 } from '@standardnotes/domain-events'
@@ -12,11 +11,13 @@ import { Predicate } from '../Predicate/Predicate'
 
 export interface DomainEventFactoryInterface {
   createPredicateVerificationRequestedEvent(job: Job, predicate: Predicate): PredicateVerificationRequestedEvent
-  createEmailMessageRequestedEvent(dto: {
+  createEmailRequestedEvent(dto: {
     userEmail: string
-    messageIdentifier: EmailMessageIdentifier
-    context: Record<string, unknown>
-  }): EmailMessageRequestedEvent
+    messageIdentifier: string
+    level: string
+    body: string
+    subject: string
+  }): EmailRequestedEvent
   createDiscountApplyRequestedEvent(dto: { userEmail: string; discountCode: string }): DiscountApplyRequestedEvent
   createDiscountWithdrawRequestedEvent(dto: { userEmail: string; discountCode: string }): DiscountWithdrawRequestedEvent
   createExitDiscountWithdrawRequestedEvent(dto: {

+ 15 - 22
packages/scheduler/src/Domain/Job/JobDoneInterpreter.spec.ts

@@ -6,6 +6,7 @@ import {
   ExitDiscountWithdrawRequestedEvent,
 } from '@standardnotes/domain-events'
 import { PredicateName } from '@standardnotes/predicates'
+import { TimerInterface } from '@standardnotes/time'
 import 'reflect-metadata'
 import { Logger } from 'winston'
 import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
@@ -26,13 +27,17 @@ describe('JobDoneInterpreter', () => {
   let domainEventPublisher: DomainEventPublisherInterface
   let job: Job
   let logger: Logger
+  let timer: TimerInterface
 
   const createInterpreter = () =>
-    new JobDoneInterpreter(jobRepository, predicateRepository, domainEventFactory, domainEventPublisher, logger)
+    new JobDoneInterpreter(jobRepository, predicateRepository, domainEventFactory, domainEventPublisher, timer, logger)
 
   beforeEach(() => {
     job = {} as jest.Mocked<Job>
 
+    timer = {} as jest.Mocked<TimerInterface>
+    timer.convertMicrosecondsToDate = jest.fn().mockReturnValue(new Date())
+
     jobRepository = {} as jest.Mocked<JobRepositoryInterface>
     jobRepository.findOneByUuid = jest.fn().mockReturnValue(job)
 
@@ -40,7 +45,7 @@ describe('JobDoneInterpreter', () => {
     predicateRepository.findByJobUuid = jest.fn().mockReturnValue([])
 
     domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
-    domainEventFactory.createEmailMessageRequestedEvent = jest
+    domainEventFactory.createEmailRequestedEvent = jest
       .fn()
       .mockReturnValue({} as jest.Mocked<EmailMessageRequestedEvent>)
     domainEventFactory.createDiscountApplyRequestedEvent = jest
@@ -89,11 +94,7 @@ describe('JobDoneInterpreter', () => {
 
     await createInterpreter().interpret('1-2-3')
 
-    expect(domainEventFactory.createEmailMessageRequestedEvent).toHaveBeenCalledWith({
-      context: {},
-      messageIdentifier: 'ENCOURAGE_EMAIL_BACKUPS',
-      userEmail: 'test@test.te',
-    })
+    expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
     expect(domainEventPublisher.publish).toHaveBeenCalled()
   })
 
@@ -111,7 +112,7 @@ describe('JobDoneInterpreter', () => {
 
     await createInterpreter().interpret('1-2-3')
 
-    expect(domainEventFactory.createEmailMessageRequestedEvent).not.toHaveBeenCalled()
+    expect(domainEventFactory.createEmailRequestedEvent).not.toHaveBeenCalled()
     expect(domainEventPublisher.publish).not.toHaveBeenCalled()
   })
 
@@ -124,7 +125,7 @@ describe('JobDoneInterpreter', () => {
 
     await createInterpreter().interpret('1-2-3')
 
-    expect(domainEventFactory.createEmailMessageRequestedEvent).not.toHaveBeenCalled()
+    expect(domainEventFactory.createEmailRequestedEvent).not.toHaveBeenCalled()
     expect(domainEventPublisher.publish).not.toHaveBeenCalled()
   })
 
@@ -143,11 +144,7 @@ describe('JobDoneInterpreter', () => {
 
     await createInterpreter().interpret('1-2-3')
 
-    expect(domainEventFactory.createEmailMessageRequestedEvent).toHaveBeenCalledWith({
-      context: { userRegisteredAt: 123 },
-      messageIdentifier: 'ENCOURAGE_SUBSCRIPTION_PURCHASING',
-      userEmail: 'test@test.te',
-    })
+    expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
     expect(domainEventPublisher.publish).toHaveBeenCalled()
   })
 
@@ -160,7 +157,7 @@ describe('JobDoneInterpreter', () => {
 
     await createInterpreter().interpret('1-2-3')
 
-    expect(domainEventFactory.createEmailMessageRequestedEvent).not.toHaveBeenCalled()
+    expect(domainEventFactory.createEmailRequestedEvent).not.toHaveBeenCalled()
     expect(domainEventPublisher.publish).not.toHaveBeenCalled()
   })
 
@@ -173,11 +170,7 @@ describe('JobDoneInterpreter', () => {
 
     await createInterpreter().interpret('1-2-3')
 
-    expect(domainEventFactory.createEmailMessageRequestedEvent).toHaveBeenCalledWith({
-      context: {},
-      messageIdentifier: 'EXIT_INTERVIEW',
-      userEmail: 'test@test.te',
-    })
+    expect(domainEventFactory.createEmailRequestedEvent).toHaveBeenCalled()
     expect(domainEventPublisher.publish).toHaveBeenCalled()
   })
 
@@ -190,7 +183,7 @@ describe('JobDoneInterpreter', () => {
 
     await createInterpreter().interpret('1-2-3')
 
-    expect(domainEventFactory.createEmailMessageRequestedEvent).not.toHaveBeenCalled()
+    expect(domainEventFactory.createEmailRequestedEvent).not.toHaveBeenCalled()
     expect(domainEventPublisher.publish).not.toHaveBeenCalled()
   })
 
@@ -295,7 +288,7 @@ describe('JobDoneInterpreter', () => {
 
     await createInterpreter().interpret('1-2-3')
 
-    expect(domainEventFactory.createEmailMessageRequestedEvent).not.toHaveBeenCalled()
+    expect(domainEventFactory.createEmailRequestedEvent).not.toHaveBeenCalled()
     expect(domainEventPublisher.publish).not.toHaveBeenCalled()
   })
 })

+ 29 - 12
packages/scheduler/src/Domain/Job/JobDoneInterpreter.ts

@@ -1,8 +1,9 @@
-import { EmailMessageIdentifier } from '@standardnotes/common'
 import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
 import { PredicateName } from '@standardnotes/predicates'
 import { inject, injectable } from 'inversify'
 import { Logger } from 'winston'
+import { EmailLevel } from '@standardnotes/domain-core'
+import { TimerInterface } from '@standardnotes/time'
 
 import TYPES from '../../Bootstrap/Types'
 import { DomainEventFactoryInterface } from '../Event/DomainEventFactoryInterface'
@@ -13,6 +14,15 @@ import { Job } from './Job'
 import { JobDoneInterpreterInterface } from './JobDoneInterpreterInterface'
 import { JobName } from './JobName'
 import { JobRepositoryInterface } from './JobRepositoryInterface'
+import { getSubject as getExitInterviewSubject, getBody as getExitInterviewBody } from '../Email/ExitInterview'
+import {
+  getSubject as getEncourageEmailBackupsSubject,
+  getBody as getEncourageEmailBackupsBody,
+} from '../Email/EncourageEmailBackups'
+import {
+  getSubject as getEncourageSubscriptionPurchasingSubject,
+  getBody as getEncourageSubscriptionPurchasingBody,
+} from '../Email/EncourageSubscriptionPurchasing'
 
 @injectable()
 export class JobDoneInterpreter implements JobDoneInterpreterInterface {
@@ -21,6 +31,7 @@ export class JobDoneInterpreter implements JobDoneInterpreterInterface {
     @inject(TYPES.PredicateRepository) private predicateRepository: PredicateRepositoryInterface,
     @inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
     @inject(TYPES.DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
+    @inject(TYPES.Timer) private timer: TimerInterface,
     @inject(TYPES.Logger) private logger: Logger,
   ) {}
 
@@ -81,10 +92,12 @@ export class JobDoneInterpreter implements JobDoneInterpreterInterface {
     this.logger.debug(`[${job.uuid}]${job.name}: requesting email backup encouragement email.`)
 
     await this.domainEventPublisher.publish(
-      this.domainEventFactory.createEmailMessageRequestedEvent({
+      this.domainEventFactory.createEmailRequestedEvent({
         userEmail: job.userIdentifier,
-        messageIdentifier: EmailMessageIdentifier.ENCOURAGE_EMAIL_BACKUPS,
-        context: {},
+        messageIdentifier: 'ENCOURAGE_EMAIL_BACKUPS',
+        subject: getEncourageEmailBackupsSubject(),
+        body: getEncourageEmailBackupsBody(),
+        level: EmailLevel.LEVELS.System,
       }),
     )
   }
@@ -93,12 +106,14 @@ export class JobDoneInterpreter implements JobDoneInterpreterInterface {
     this.logger.debug(`[${job.uuid}]${job.name}: requesting subscription purchase encouragement email.`)
 
     await this.domainEventPublisher.publish(
-      this.domainEventFactory.createEmailMessageRequestedEvent({
+      this.domainEventFactory.createEmailRequestedEvent({
         userEmail: job.userIdentifier,
-        messageIdentifier: EmailMessageIdentifier.ENCOURAGE_SUBSCRIPTION_PURCHASING,
-        context: {
-          userRegisteredAt: job.createdAt,
-        },
+        messageIdentifier: 'ENCOURAGE_SUBSCRIPTION_PURCHASING',
+        subject: getEncourageSubscriptionPurchasingSubject(),
+        body: getEncourageSubscriptionPurchasingBody(
+          this.timer.convertMicrosecondsToDate(job.createdAt).toLocaleString(),
+        ),
+        level: EmailLevel.LEVELS.System,
       }),
     )
   }
@@ -107,10 +122,12 @@ export class JobDoneInterpreter implements JobDoneInterpreterInterface {
     this.logger.debug(`[${job.uuid}]${job.name}: requesting exit interview email.`)
 
     await this.domainEventPublisher.publish(
-      this.domainEventFactory.createEmailMessageRequestedEvent({
+      this.domainEventFactory.createEmailRequestedEvent({
         userEmail: job.userIdentifier,
-        messageIdentifier: EmailMessageIdentifier.EXIT_INTERVIEW,
-        context: {},
+        messageIdentifier: 'EXIT_INTERVIEW',
+        subject: getExitInterviewSubject(),
+        body: getExitInterviewBody(),
+        level: EmailLevel.LEVELS.System,
       }),
     )
   }

+ 1 - 0
yarn.lock

@@ -2284,6 +2284,7 @@ __metadata:
     "@newrelic/winston-enricher": "npm:^4.0.0"
     "@sentry/node": "npm:^7.19.0"
     "@standardnotes/common": "workspace:*"
+    "@standardnotes/domain-core": "workspace:^"
     "@standardnotes/domain-events": "workspace:*"
     "@standardnotes/domain-events-infra": "workspace:*"
     "@standardnotes/predicates": "workspace:*"