Merge pull request #23457 from overleaf/ar-recurly-account-mapping-initial

[web] setup Recurly -> Mongo account mapping

GitOrigin-RevId: ee08cad60ee04e62100f3d5a4f76fdbcf5543917
This commit is contained in:
Andrew Rumble 2025-02-20 12:03:49 +00:00 committed by Copybot
parent be247b8cc9
commit 0e64fe2b21
10 changed files with 559 additions and 308 deletions

View file

@ -0,0 +1,97 @@
const mappings = new Map([
['salesforce_id', generateSubscriptionToSalesforceMapping],
['v1_id', generateSubscriptionToV1Mapping],
['recurlySubscription_id', generateSubscriptionToRecurlyMapping],
])
/**
* @typedef {(import('./types.d.ts').AccountMapping)} AccountMapping
*/
/**
*
* @param {Object} subscription
* @param {Object} updatedSubscription
* @return {Array<AccountMapping>}
*/
function extractAccountMappingsFromSubscription(
subscription,
updatedSubscription
) {
const accountMappings = []
mappings.forEach((generateMapping, param) => {
if (updatedSubscription[param] || updatedSubscription[param] === '') {
if (subscription[param] !== updatedSubscription[param]) {
accountMappings.push(
generateMapping(subscription.id, updatedSubscription[param])
)
}
}
})
return accountMappings
}
function generateV1Mapping(v1Id, salesforceId, createdAt) {
return {
source: 'salesforce',
sourceEntity: 'account',
sourceEntityId: salesforceId,
target: 'v1',
targetEntity: 'university',
targetEntityId: v1Id,
createdAt,
}
}
function generateSubscriptionToV1Mapping(subscriptionId, v1Id) {
return {
source: 'v1',
sourceEntity: 'university',
sourceEntityId: v1Id,
target: 'v2',
targetEntity: 'subscription',
targetEntityId: subscriptionId,
createdAt: new Date().toISOString(),
}
}
function generateSubscriptionToSalesforceMapping(subscriptionId, salesforceId) {
return {
source: 'salesforce',
sourceEntity: 'account',
sourceEntityId: salesforceId,
target: 'v2',
targetEntity: 'subscription',
targetEntityId: subscriptionId,
createdAt: new Date().toISOString(),
}
}
/**
*
* @param {string} subscriptionId
* @param {string} recurlyId
* @param {string} [createdAt] - Should be an ISO date
* @return {AccountMapping}
*/
function generateSubscriptionToRecurlyMapping(
subscriptionId,
recurlyId,
createdAt = new Date().toISOString()
) {
return {
source: 'recurly',
sourceEntity: 'subscription',
sourceEntityId: recurlyId,
target: 'v2',
targetEntity: 'subscription',
targetEntityId: subscriptionId,
createdAt,
}
}
module.exports = {
extractAccountMappingsFromSubscription,
generateV1Mapping,
generateSubscriptionToRecurlyMapping,
}

View file

@ -1,71 +0,0 @@
export function extractAccountMappingsFromSubscription(
subscription,
updatedSubscription
) {
const accountMappings = []
if (
updatedSubscription.salesforce_id ||
updatedSubscription.salesforce_id === ''
) {
if (subscription.salesforce_id !== updatedSubscription.salesforce_id) {
accountMappings.push(
generateSubscriptionToSalesforceMapping(
subscription.id,
updatedSubscription.salesforce_id
)
)
}
}
if (updatedSubscription.v1_id || updatedSubscription.v1_id === '') {
if (subscription.v1_id !== updatedSubscription.v1_id) {
accountMappings.push(
generateSubscriptionToV1Mapping(
subscription.id,
updatedSubscription.v1_id
)
)
}
}
return accountMappings
}
export function generateV1Mapping(v1Id, salesforceId, createdAt) {
return {
source: 'salesforce',
sourceEntity: 'account',
sourceEntityId: salesforceId,
target: 'v1',
targetEntity: 'university',
targetEntityId: v1Id,
createdAt,
}
}
function generateSubscriptionToV1Mapping(subscriptionId, v1Id) {
return {
source: 'v1',
sourceEntity: 'university',
sourceEntityId: v1Id,
target: 'v2',
targetEntity: 'subscription',
targetEntityId: subscriptionId,
createdAt: new Date().toISOString(),
}
}
function generateSubscriptionToSalesforceMapping(subscriptionId, salesforceId) {
return {
source: 'salesforce',
sourceEntity: 'account',
sourceEntityId: salesforceId,
target: 'v2',
targetEntity: 'subscription',
targetEntityId: subscriptionId,
createdAt: new Date().toISOString(),
}
}
export default {
extractAccountMappingsFromSubscription,
generateV1Mapping,
}

View file

@ -4,7 +4,7 @@ import SessionManager from '../Authentication/SessionManager.js'
import GeoIpLookup from '../../infrastructure/GeoIpLookup.js'
import Features from '../../infrastructure/Features.js'
import { expressify } from '@overleaf/promise-utils'
import { generateV1Mapping } from './AccountMappingHelper.mjs'
import AccountMappingHelper from './AccountMappingHelper.js'
async function registerSalesforceMapping(req, res, next) {
if (!Features.hasFeature('analytics')) {
@ -12,7 +12,7 @@ async function registerSalesforceMapping(req, res, next) {
}
const { createdAt, salesforceId, v1Id } = req.body
AnalyticsManager.registerAccountMapping(
generateV1Mapping(v1Id, salesforceId, createdAt)
AccountMappingHelper.generateV1Mapping(v1Id, salesforceId, createdAt)
)
res.sendStatus(202)
}

View file

@ -146,16 +146,14 @@ function setUserPropertyForSessionInBackground(session, property, value) {
})
}
/**
* @typedef {(import('./types').AccountMapping)} AccountMapping
*/
/**
* Register mapping between two accounts.
*
* @param {object} payload - The event payload to send to Analytics
* @param {string} payload.source - The type of account linked from
* @param {string} payload.sourceId - The ID of the account linked from
* @param {string} payload.target - The type of account linked to
* @param {string} payload.targetId - The ID of the account linked to
* @param {Date} payload.createdAt - The date the mapping was created
* @property
* @param {AccountMapping} payload - The event payload to send to Analytics
*/
function registerAccountMapping({
source,

View file

@ -0,0 +1,9 @@
export type AccountMapping = {
source: string
sourceEntity: string
sourceEntityId: string
target: string
targetEntity: string
targetEntityId: string
createdAt: string
}

View file

@ -10,6 +10,7 @@ const { DeletedSubscription } = require('../../models/DeletedSubscription')
const logger = require('@overleaf/logger')
const Features = require('../../infrastructure/Features')
const UserAuditLogHandler = require('../User/UserAuditLogHandler')
const AccountMappingHelper = require('../Analytics/AccountMappingHelper')
const { SSOConfig } = require('../../models/SSOConfig')
/**
@ -345,6 +346,16 @@ async function updateSubscriptionFromRecurly(
}
}
await subscription.save()
const accountMapping =
AccountMappingHelper.generateSubscriptionToRecurlyMapping(
subscription._id,
subscription.recurlySubscription_id
)
if (accountMapping) {
AnalyticsManager.registerAccountMapping(accountMapping)
}
await _scheduleRefreshFeatures(subscription)
}

View file

@ -0,0 +1,120 @@
/**
* This script backfills the account mapping for subscriptions that are active and have a group plan.
*
* The mapping joins a recurlySubscription_id to a subscription _id in BigQuery.
*
* This script has an assumption that it is being run in a clean slate condition, it will create some
* duplicate mappings if run multiple times. The Analytics team will have the expectation
* that this table may need to be deduplicated as it is an event sourcing record.
*
* Call it with `--commit` to actually register the mappings.
* Call it with `--verbose` to see debug logs.
* Call it with `--endDate=<subscription ID>` to stop processing at a certain date
*/
import logger from '@overleaf/logger'
import minimist from 'minimist'
import { batchedUpdate } from '@overleaf/mongo-utils/batchedUpdate.js'
import { db } from '../../app/src/infrastructure/mongodb.js'
import AccountMappingHelper from '../../app/src/Features/Analytics/AccountMappingHelper.js'
import { registerAccountMapping } from '../../app/src/Features/Analytics/AnalyticsManager.js'
import { triggerGracefulShutdown } from '../../app/src/infrastructure/GracefulShutdown.js'
import Validation from '../../app/src/infrastructure/Validation.js'
const paramsSchema = Validation.Joi.object({
endDate: Validation.Joi.string().isoDate(),
commit: Validation.Joi.boolean().default(false),
verbose: Validation.Joi.boolean().default(false),
}).unknown(true)
let mapped = 0
let subscriptionCount = 0
const now = new Date().toISOString() // use the same timestamp for all mappings
const seenSubscriptions = new Set()
function registerMapping(subscription) {
if (seenSubscriptions.has(subscription._id)) {
logger.warn({ subscription }, 'duplicate subscription found, skipping')
return
}
seenSubscriptions.add(subscription._id)
subscriptionCount++
const mapping = AccountMappingHelper.generateSubscriptionToRecurlyMapping(
subscription._id,
subscription.recurlySubscription_id,
now
)
logger.debug(
{
recurly: subscription.recurlySubscription_id,
mapping,
},
`processing subscription ${subscription._id}`
)
if (commit) {
registerAccountMapping(mapping)
mapped++
}
}
async function main() {
const additionalBatchedUpdateOptions = {}
if (endDate) {
additionalBatchedUpdateOptions.BATCH_RANGE_END = endDate
}
await batchedUpdate(
db.subscriptions,
{
'recurlyStatus.state': 'active',
groupPlan: true,
},
subscriptions => subscriptions.forEach(registerMapping),
{
_id: 1,
recurlySubscription_id: 1,
},
{
readPreference: 'secondaryPreferred',
},
{
verboseLogging: verbose,
...additionalBatchedUpdateOptions,
}
)
logger.debug({}, `${subscriptionCount} subscriptions processed`)
if (commit) {
logger.debug({}, `${mapped} mappings registered`)
}
}
const {
error,
value: { commit, endDate, verbose },
} = paramsSchema.validate(
minimist(process.argv.slice(2), {
boolean: ['commit', 'verbose'],
string: ['endDate'],
})
)
logger.logger.level(verbose ? 'debug' : 'info')
if (error) {
logger.error({ error }, 'error with parameters')
triggerGracefulShutdown(done => done(1))
} else {
logger.info({ verbose, commit, endDate }, commit ? 'COMMITTING' : 'DRY RUN')
await main()
triggerGracefulShutdown({
close(done) {
logger.info({}, 'shutting down')
done()
},
})
}

View file

@ -0,0 +1,276 @@
const SandboxedModule = require('sandboxed-module')
const { expect } = require('chai')
const { ObjectId } = require('mongodb-legacy')
const path = require('node:path')
const MODULE_PATH = path.join(
__dirname,
'../../../../app/src/Features/Analytics/AccountMappingHelper'
)
describe('AccountMappingHelper', function () {
beforeEach(function () {
this.AccountMappingHelper = SandboxedModule.require(MODULE_PATH)
})
describe('extractAccountMappingsFromSubscription', function () {
describe('when the v1 id is the same in the updated subscription and the subscription', function () {
describe('when the salesforce id is the same in the updated subscription and the subscription', function () {
beforeEach(function () {
this.subscription = {
id: new ObjectId('abc123abc123abc123abc123'),
salesforce_id: 'def456def456def456',
}
this.updatedSubscription = { salesforce_id: 'def456def456def456' }
this.result =
this.AccountMappingHelper.extractAccountMappingsFromSubscription(
this.subscription,
this.updatedSubscription
)
})
it('returns an empty array', function () {
expect(this.result).to.be.an('array')
expect(this.result).to.have.length(0)
})
})
describe('when the salesforce id has changed between the subscription and the updated subscription', function () {
beforeEach(function () {
this.subscription = {
id: new ObjectId('abc123abc123abc123abc123'),
salesforce_id: 'def456def456def456',
}
this.updatedSubscription = { salesforce_id: 'ghi789ghi789ghi789' }
this.result =
this.AccountMappingHelper.extractAccountMappingsFromSubscription(
this.subscription,
this.updatedSubscription
)
})
it('returns an array with a single item', function () {
expect(this.result).to.be.an('array')
expect(this.result).to.have.length(1)
})
it('uses "account" as sourceEntity', function () {
expect(this.result[0]).to.haveOwnProperty('sourceEntity', 'account')
})
it('uses the salesforceId from the updated subscription as sourceEntityId', function () {
expect(this.result[0]).to.haveOwnProperty(
'sourceEntityId',
this.updatedSubscription.salesforce_id
)
})
it('uses "subscription" as targetEntity', function () {
expect(this.result[0]).to.haveOwnProperty(
'targetEntity',
'subscription'
)
})
it('uses the subscriptionId as targetEntityId', function () {
expect(this.result[0]).to.haveOwnProperty(
'targetEntityId',
this.subscription.id
)
})
})
describe('when the update subscription has a salesforce id and the subscription has no salesforce_id', function () {
beforeEach(function () {
this.subscription = { id: new ObjectId('abc123abc123abc123abc123') }
this.updatedSubscription = { salesforce_id: 'def456def456def456' }
this.result =
this.AccountMappingHelper.extractAccountMappingsFromSubscription(
this.subscription,
this.updatedSubscription
)
})
it('returns an array with a single item', function () {
expect(this.result).to.be.an('array')
expect(this.result).to.have.length(1)
})
it('uses "account" as sourceEntity', function () {
expect(this.result[0]).to.haveOwnProperty('sourceEntity', 'account')
})
it('uses the salesforceId from the updated subscription as sourceEntityId', function () {
expect(this.result[0]).to.haveOwnProperty(
'sourceEntityId',
this.updatedSubscription.salesforce_id
)
})
it('uses "subscription" as targetEntity', function () {
expect(this.result[0]).to.haveOwnProperty(
'targetEntity',
'subscription'
)
})
it('uses the subscriptionId as targetEntityId', function () {
expect(this.result[0]).to.haveOwnProperty(
'targetEntityId',
this.subscription.id
)
})
})
})
describe('when the v1 id has changed between the subscription and the updated subscription', function () {
describe('when the salesforce id has not changed between the subscription and the updated subscription', function () {
beforeEach(function () {
this.subscription = {
id: new ObjectId('abc123abc123abc123abc123'),
v1_id: '1',
salesforce_id: '',
}
this.updatedSubscription = { v1_id: '2', salesforce_id: '' }
this.result =
this.AccountMappingHelper.extractAccountMappingsFromSubscription(
this.subscription,
this.updatedSubscription
)
})
it('returns an array with a single item', function () {
expect(this.result).to.be.an('array')
expect(this.result).to.have.length(1)
})
it('uses "university" as the sourceEntity', function () {
expect(this.result[0]).to.haveOwnProperty(
'sourceEntity',
'university'
)
})
it('uses the v1_id from the updated subscription as the sourceEntityId', function () {
expect(this.result[0]).to.haveOwnProperty(
'sourceEntityId',
this.updatedSubscription.v1_id
)
})
it('uses "subscription" as the targetEntity', function () {
expect(this.result[0]).to.haveOwnProperty(
'targetEntity',
'subscription'
)
})
it('uses the subscription id as the targetEntityId', function () {
expect(this.result[0]).to.haveOwnProperty(
'targetEntityId',
this.subscription.id
)
})
})
describe('when the salesforce id has changed between the subscription and the updated subscription', function () {
beforeEach(function () {
this.subscription = {
id: new ObjectId('abc123abc123abc123abc123'),
v1_id: '',
salesforce_id: 'def456def456def456',
}
this.updatedSubscription = {
v1_id: '2',
salesforce_id: '',
}
this.result =
this.AccountMappingHelper.extractAccountMappingsFromSubscription(
this.subscription,
this.updatedSubscription
)
})
it('returns an array with two items', function () {
expect(this.result).to.be.an('array')
expect(this.result).to.have.length(2)
})
it('uses the salesforce_id from the updated subscription as the sourceEntityId for the first item', function () {
expect(this.result[0]).to.haveOwnProperty(
'sourceEntityId',
this.updatedSubscription.salesforce_id
)
})
it('uses the subscription id as the targetEntityId for the first item', function () {
expect(this.result[0]).to.haveOwnProperty(
'targetEntityId',
this.subscription.id
)
})
it('uses the v1_id from the updated subscription as the sourceEntityId for the second item', function () {
expect(this.result[1]).to.haveOwnProperty(
'sourceEntityId',
this.updatedSubscription.v1_id
)
})
it('uses the subscription id as the targetEntityId for the second item', function () {
expect(this.result[1]).to.haveOwnProperty(
'targetEntityId',
this.subscription.id
)
})
})
})
})
describe('when the recurlySubscription_id has changed between the subscription and the updated subscription', function () {
beforeEach(function () {
this.subscription = {
id: new ObjectId('abc123abc123abc123abc123'),
recurlySubscription_id: '',
}
this.updatedSubscription = {
recurlySubscription_id: '1234a5678b90123cd4567e8f901a2b34',
}
this.result =
this.AccountMappingHelper.extractAccountMappingsFromSubscription(
this.subscription,
this.updatedSubscription
)
})
it('returns an array with one item', function () {
expect(this.result).to.be.an('array')
expect(this.result).to.have.length(1)
})
it('uses "recurly" as the source', function () {
expect(this.result[0]).to.haveOwnProperty('source', 'recurly')
})
it('uses "subscription" as the sourceEntity', function () {
expect(this.result[0]).to.haveOwnProperty('sourceEntity', 'subscription')
})
it('uses the recurlySubscription_id as the sourceEntityId', function () {
expect(this.result[0]).to.haveOwnProperty(
'sourceEntityId',
this.updatedSubscription.recurlySubscription_id
)
})
it('uses "v2" as the target', function () {
expect(this.result[0]).to.haveOwnProperty('target', 'v2')
})
it('uses "subscription" as the targetEntity', function () {
expect(this.result[0]).to.haveOwnProperty('targetEntity', 'subscription')
})
it('uses the subscription id as the targetEntityId', function () {
expect(this.result[0]).to.haveOwnProperty(
'targetEntityId',
this.subscription.id
)
})
})
})

View file

@ -1,228 +0,0 @@
import path from 'node:path'
import esmock from 'esmock'
import { expect } from 'chai'
import mongodb from 'mongodb-legacy'
import { fileURLToPath } from 'node:url'
const { ObjectId } = mongodb
const __dirname = fileURLToPath(new URL('.', import.meta.url))
const MODULE_PATH = path.join(
__dirname,
'../../../../app/src/Features/Analytics/AccountMappingHelper'
)
describe('AccountMappingHelper', function () {
beforeEach(async function () {
this.AccountMappingHelper = await esmock.strict(MODULE_PATH)
})
describe('extractAccountMappingsFromSubscription', function () {
describe('when the v1 id is the same in the updated subscription and the subscription', function () {
describe('when the salesforce id is the same in the updated subscription and the subscription', function () {
beforeEach(function () {
this.subscription = {
id: new ObjectId('abc123abc123abc123abc123'),
salesforce_id: 'def456def456def456',
}
this.updatedSubscription = { salesforce_id: 'def456def456def456' }
this.result =
this.AccountMappingHelper.extractAccountMappingsFromSubscription(
this.subscription,
this.updatedSubscription
)
})
it('returns an empty array', function () {
expect(this.result).to.be.an('array')
expect(this.result).to.have.length(0)
})
})
describe('when the salesforce id has changed between the subscription and the updated subscription', function () {
beforeEach(function () {
this.subscription = {
id: new ObjectId('abc123abc123abc123abc123'),
salesforce_id: 'def456def456def456',
}
this.updatedSubscription = { salesforce_id: 'ghi789ghi789ghi789' }
this.result =
this.AccountMappingHelper.extractAccountMappingsFromSubscription(
this.subscription,
this.updatedSubscription
)
})
it('returns an array with a single item', function () {
expect(this.result).to.be.an('array')
expect(this.result).to.have.length(1)
})
it('uses "account" as sourceEntity', function () {
expect(this.result[0]).to.haveOwnProperty('sourceEntity', 'account')
})
it('uses the salesforceId from the updated subscription as sourceEntityId', function () {
expect(this.result[0]).to.haveOwnProperty(
'sourceEntityId',
this.updatedSubscription.salesforce_id
)
})
it('uses "subscription" as targetEntity', function () {
expect(this.result[0]).to.haveOwnProperty(
'targetEntity',
'subscription'
)
})
it('uses the subscriptionId as targetEntityId', function () {
expect(this.result[0]).to.haveOwnProperty(
'targetEntityId',
this.subscription.id
)
})
})
describe('when the update subscription has a salesforce id and the subscription has no salesforce_id', function () {
beforeEach(function () {
this.subscription = { id: new ObjectId('abc123abc123abc123abc123') }
this.updatedSubscription = { salesforce_id: 'def456def456def456' }
this.result =
this.AccountMappingHelper.extractAccountMappingsFromSubscription(
this.subscription,
this.updatedSubscription
)
})
it('returns an array with a single item', function () {
expect(this.result).to.be.an('array')
expect(this.result).to.have.length(1)
})
it('uses "account" as sourceEntity', function () {
expect(this.result[0]).to.haveOwnProperty('sourceEntity', 'account')
})
it('uses the salesforceId from the updated subscription as sourceEntityId', function () {
expect(this.result[0]).to.haveOwnProperty(
'sourceEntityId',
this.updatedSubscription.salesforce_id
)
})
it('uses "subscription" as targetEntity', function () {
expect(this.result[0]).to.haveOwnProperty(
'targetEntity',
'subscription'
)
})
it('uses the subscriptionId as targetEntityId', function () {
expect(this.result[0]).to.haveOwnProperty(
'targetEntityId',
this.subscription.id
)
})
})
})
})
describe('when the v1 id has changed between the subscription and the updated subscription', function () {
describe('when the salesforce id has not changed between the subscription and the updated subscription', function () {
beforeEach(function () {
this.subscription = {
id: new ObjectId('abc123abc123abc123abc123'),
v1_id: '1',
salesforce_id: '',
}
this.updatedSubscription = { v1_id: '2', salesforce_id: '' }
this.result =
this.AccountMappingHelper.extractAccountMappingsFromSubscription(
this.subscription,
this.updatedSubscription
)
})
it('returns an array with a single item', function () {
expect(this.result).to.be.an('array')
expect(this.result).to.have.length(1)
})
it('uses "university" as the sourceEntity', function () {
expect(this.result[0]).to.haveOwnProperty('sourceEntity', 'university')
})
it('uses the v1_id from the updated subscription as the sourceEntityId', function () {
expect(this.result[0]).to.haveOwnProperty(
'sourceEntityId',
this.updatedSubscription.v1_id
)
})
it('uses "subscription" as the targetEntity', function () {
expect(this.result[0]).to.haveOwnProperty(
'targetEntity',
'subscription'
)
})
it('uses the subscription id as the targetEntityId', function () {
expect(this.result[0]).to.haveOwnProperty(
'targetEntityId',
this.subscription.id
)
})
})
describe('when the salesforce id has changed between the subscription and the updated subscription', function () {
beforeEach(function () {
this.subscription = {
id: new ObjectId('abc123abc123abc123abc123'),
v1_id: '',
salesforce_id: 'def456def456def456',
}
this.updatedSubscription = {
v1_id: '2',
salesforce_id: '',
}
this.result =
this.AccountMappingHelper.extractAccountMappingsFromSubscription(
this.subscription,
this.updatedSubscription
)
})
it('returns an array with two items', function () {
expect(this.result).to.be.an('array')
expect(this.result).to.have.length(2)
})
it('uses the salesforce_id from the updated subscription as the sourceEntityId for the first item', function () {
expect(this.result[0]).to.haveOwnProperty(
'sourceEntityId',
this.updatedSubscription.salesforce_id
)
})
it('uses the subscription id as the targetEntityId for the first item', function () {
expect(this.result[0]).to.haveOwnProperty(
'targetEntityId',
this.subscription.id
)
})
it('uses the v1_id from the updated subscription as the sourceEntityId for the second item', function () {
expect(this.result[1]).to.haveOwnProperty(
'sourceEntityId',
this.updatedSubscription.v1_id
)
})
it('uses the subscription id as the targetEntityId for the second item', function () {
expect(this.result[1]).to.haveOwnProperty(
'targetEntityId',
this.subscription.id
)
})
})
})
})

View file

@ -148,6 +148,7 @@ describe('SubscriptionUpdater', function () {
this.AnalyticsManager = {
recordEventForUserInBackground: sinon.stub().resolves(),
setUserPropertyForUserInBackground: sinon.stub(),
registerAccountMapping: sinon.stub(),
}
this.Features = {
@ -175,6 +176,9 @@ describe('SubscriptionUpdater', function () {
DeletedSubscription: this.DeletedSubscription,
},
'../Analytics/AnalyticsManager': this.AnalyticsManager,
'../Analytics/AccountMappingHelper': (this.AccountMappingHelper = {
generateSubscriptionToRecurlyMapping: sinon.stub(),
}),
'../../infrastructure/Features': this.Features,
'../User/UserAuditLogHandler': this.UserAuditLogHandler,
},
@ -280,6 +284,41 @@ describe('SubscriptionUpdater', function () {
).to.have.been.calledWith(this.adminUser._id)
})
it('should send a recurly account mapping event', async function () {
const createdAt = new Date().toISOString()
this.AccountMappingHelper.generateSubscriptionToRecurlyMapping.returns({
source: 'recurly',
sourceEntity: 'subscription',
sourceEntityId: this.recurlySubscription.uuid,
target: 'v2',
targetEntity: 'subscription',
targetEntityId: this.subscription._id,
createdAt,
})
await this.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(
this.recurlySubscription,
this.subscription,
{}
)
expect(
this.AccountMappingHelper.generateSubscriptionToRecurlyMapping
).to.have.been.calledWith(
this.subscription._id,
this.recurlySubscription.uuid
)
expect(
this.AnalyticsManager.registerAccountMapping
).to.have.been.calledWith({
source: 'recurly',
sourceEntity: 'subscription',
sourceEntityId: this.recurlySubscription.uuid,
target: 'v2',
targetEntity: 'subscription',
targetEntityId: this.subscription._id,
createdAt,
})
})
it('should remove the subscription when expired', async function () {
this.recurlySubscription.state = 'expired'
await this.SubscriptionUpdater.promises.updateSubscriptionFromRecurly(