فهرست منبع

Merge pull request #34730 from overleaf/rh-tax-id-verify

Validate tax ids and send emails on failed verification

GitOrigin-RevId: 84d53fdaf00d7b23ced22dd540a071e3028ced93
roo hutton 1 ماه پیش
والد
کامیت
0fab15c58e

+ 45 - 0
services/web/app/src/Features/Email/EmailBuilder.mjs

@@ -1008,6 +1008,51 @@ templates.taxExemptCertificateRequired = NoCTAEmailTemplate({
   },
   },
 })
 })
 
 
+function taxIdInvalidTemplate({ noun, descriptor }) {
+  return ctaTemplate({
+    subject() {
+      return `Action required: Update your ${noun} on ${settings.appName}`
+    },
+    title() {
+      return `Action required: Update your ${noun}`
+    },
+    greeting() {
+      return 'Hi,'
+    },
+    message() {
+      return [
+        `We're writing to let you know that the ${descriptor} you entered did not validate successfully.`,
+        `Please update your ${noun} on your billing information page:`,
+      ]
+    },
+    secondaryMessage() {
+      return [
+        `If you do not update your ${noun}, your next invoice may be subject to additional taxes.`,
+        'If you require a new invoice, please let us know by replying to this email.',
+      ]
+    },
+    ctaText() {
+      return `Update ${noun}`
+    },
+    ctaURL() {
+      return `${settings.siteUrl}/user/subscription`
+    },
+    footerMessage(opts) {
+      return `Our reference: ${opts.stripeCustomerId}`
+    },
+  })
+}
+
+templates.taxIdInvalidVat = taxIdInvalidTemplate({
+  noun: 'VAT number',
+  descriptor: 'VAT number',
+})
+
+templates.taxIdInvalidNonVat = taxIdInvalidTemplate({
+  noun: 'tax ID',
+  descriptor: 'tax identifier',
+})
+
 templates.groupMemberLimitWarning = ctaTemplate({
 templates.groupMemberLimitWarning = ctaTemplate({
   subject(opts) {
   subject(opts) {
     return `Action needed: Your Overleaf group is nearly out of licenses`
     return `Action needed: Your Overleaf group is nearly out of licenses`

+ 85 - 0
services/web/test/unit/src/Email/EmailBuilder.test.mjs

@@ -900,6 +900,91 @@ describe('EmailBuilder', function () {
         })
         })
       })
       })
 
 
+      describe('taxIdInvalidVat', function () {
+        beforeEach(function (ctx) {
+          ctx.emailAddress = 'customer@example.com'
+          ctx.opts = {
+            to: ctx.emailAddress,
+            stripeCustomerId: 'cus_123456789',
+          }
+          ctx.email = ctx.EmailBuilder.buildEmail('taxIdInvalidVat', ctx.opts)
+          ctx.dom = cheerio.load(ctx.email.html)
+        })
+
+        it('should build the email', function (ctx) {
+          expect(ctx.email.html).to.exist
+          expect(ctx.email.text).to.exist
+        })
+
+        describe('HTML email', function () {
+          it('should include the Stripe customer ID', function (ctx) {
+            expect(ctx.email.html).to.contain(ctx.opts.stripeCustomerId)
+          })
+
+          it('should refer to the VAT number', function (ctx) {
+            expect(ctx.email.html).to.contain('VAT number')
+          })
+
+          it('should link to the subscription page', function (ctx) {
+            expect(ctx.dom('a').attr('href')).to.contain('/user/subscription')
+          })
+        })
+
+        describe('plain text email', function () {
+          it('should include the Stripe customer ID', function (ctx) {
+            expect(ctx.email.text).to.contain(ctx.opts.stripeCustomerId)
+          })
+
+          it('should refer to the VAT number', function (ctx) {
+            expect(ctx.email.text).to.contain('VAT number')
+          })
+        })
+      })
+
+      describe('taxIdInvalidNonVat', function () {
+        beforeEach(function (ctx) {
+          ctx.emailAddress = 'customer@example.com'
+          ctx.opts = {
+            to: ctx.emailAddress,
+            stripeCustomerId: 'cus_123456789',
+          }
+          ctx.email = ctx.EmailBuilder.buildEmail(
+            'taxIdInvalidNonVat',
+            ctx.opts
+          )
+          ctx.dom = cheerio.load(ctx.email.html)
+        })
+
+        it('should build the email', function (ctx) {
+          expect(ctx.email.html).to.exist
+          expect(ctx.email.text).to.exist
+        })
+
+        describe('HTML email', function () {
+          it('should include the Stripe customer ID', function (ctx) {
+            expect(ctx.email.html).to.contain(ctx.opts.stripeCustomerId)
+          })
+
+          it('should refer to the tax ID', function (ctx) {
+            expect(ctx.email.html).to.contain('tax ID')
+          })
+
+          it('should link to the subscription page', function (ctx) {
+            expect(ctx.dom('a').attr('href')).to.contain('/user/subscription')
+          })
+        })
+
+        describe('plain text email', function () {
+          it('should include the Stripe customer ID', function (ctx) {
+            expect(ctx.email.text).to.contain(ctx.opts.stripeCustomerId)
+          })
+
+          it('should refer to the tax ID', function (ctx) {
+            expect(ctx.email.text).to.contain('tax ID')
+          })
+        })
+      })
+
       describe('groupMemberLimitWarning', function () {
       describe('groupMemberLimitWarning', function () {
         beforeEach(function (ctx) {
         beforeEach(function (ctx) {
           ctx.emailAddress = 'example@overleaf.com'
           ctx.emailAddress = 'example@overleaf.com'

+ 13 - 0
services/web/types/stripe/webhook-event.ts

@@ -145,6 +145,18 @@ export interface CustomerUpdatedWebhookEvent extends Stripe.EventBase {
   }
   }
 }
 }
 
 
+export interface CustomerTaxIdUpdatedWebhookEvent extends Stripe.EventBase {
+  type: 'customer.tax_id.updated'
+  data: {
+    object: Stripe.TaxId
+    previous_attributes?: {
+      verification?: {
+        status?: Stripe.TaxId.Verification.Status
+      }
+    }
+  }
+}
+
 export type MandateUpdatedWebhookEvent = Stripe.MandateUpdatedEvent
 export type MandateUpdatedWebhookEvent = Stripe.MandateUpdatedEvent
 
 
 export type CustomerSubscriptionWebhookEvent =
 export type CustomerSubscriptionWebhookEvent =
@@ -162,4 +174,5 @@ export type WebhookEvent =
   | InvoiceOverdueWebhookEvent
   | InvoiceOverdueWebhookEvent
   | CustomerCreatedWebhookEvent
   | CustomerCreatedWebhookEvent
   | CustomerUpdatedWebhookEvent
   | CustomerUpdatedWebhookEvent
+  | CustomerTaxIdUpdatedWebhookEvent
   | MandateUpdatedWebhookEvent
   | MandateUpdatedWebhookEvent