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

Validate tax ids and send emails on failed verification

GitOrigin-RevId: 84d53fdaf00d7b23ced22dd540a071e3028ced93
This commit is contained in:
roo hutton
2026-06-25 08:05:34 +00:00
committed by Copybot
parent 29c8717b60
commit 0fab15c58e
3 changed files with 143 additions and 0 deletions
@@ -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({
subject(opts) {
return `Action needed: Your Overleaf group is nearly out of licenses`
@@ -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 () {
beforeEach(function (ctx) {
ctx.emailAddress = 'example@overleaf.com'
@@ -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 CustomerSubscriptionWebhookEvent =
@@ -162,4 +174,5 @@ export type WebhookEvent =
| InvoiceOverdueWebhookEvent
| CustomerCreatedWebhookEvent
| CustomerUpdatedWebhookEvent
| CustomerTaxIdUpdatedWebhookEvent
| MandateUpdatedWebhookEvent