Verified Commit ef3f6d54 authored by aguestuser's avatar aguestuser

[215] streamline registrar-level recycle request logic

* rename `enqueueRecyclablePhoneNumber` to `requestRecycle`
* refactor it a bit to streamline (eliminate db call & extra
  conditional by leveraging `findOrCreate` semantics in repository layer)
* move it to the `registrar.phoneNumber.recycle` module
* elimininate the `registrar.phoneNumber.enqueRecycle` module
parent 1b45c7ac
......@@ -69,8 +69,8 @@ const routesOf = async router => {
router.post('/phoneNumbers/recycle', async ctx => {
const { phoneNumbers } = ctx.request.body
const result = await phoneNumberService.enqueueRecycleablePhoneNumber({
phoneNumbers,
const result = await phoneNumberService.requestToRecycle({
phoneNumbers: phoneNumbers.split(','),
})
merge(ctx, { status: httpStatusOfMany(result), body: result })
})
......
const signal = require('../../signal')
const { isEmpty } = require('lodash')
const { statuses } = require('../../util')
const { messagesIn } = require('../../dispatcher/strings/messages')
const { findByPhoneNumber, enqueue } = require('../../db/repositories/recycleablePhoneNumber')
const channelRepository = require('../../db/repositories/channel')
// ({ string }) -> SignalboostStatus
const enqueueRecycleablePhoneNumber = async ({ phoneNumbers }) => {
return await Promise.all(
phoneNumbers.split(',').map(async phoneNumber => {
try {
const channel = await channelRepository.findDeep(phoneNumber)
if (isEmpty(channel))
return {
status: statuses.ERROR,
message: `${phoneNumber} must be associated with a channel in order to be recycled.`,
}
const recycleablePhoneNumber = await findByPhoneNumber(phoneNumber)
if (!isEmpty(recycleablePhoneNumber))
return {
status: statuses.ERROR,
message: `${phoneNumber} has already been enqueued for recycling.`,
}
await notifyAdmins(channel)
await enqueue(channel.phoneNumber)
return {
status: statuses.SUCCESS,
message: `Successfully enqueued ${phoneNumber} for recycling.`,
}
} catch (e) {
return {
status: statuses.ERROR,
message: `There was an error trying to enqueue ${phoneNumber} for recycling.`,
}
}
}),
)
}
const notifyAdmins = async channel => {
const recipients = await channelRepository.getAdminMemberships(channel)
return Promise.all(
recipients.map(recipient =>
signal.sendMessage(
recipient.memberPhoneNumber,
signal.sdMessageOf(
channel,
messagesIn(recipient.language).notifications.channelEnqueuedForRecycling,
),
),
),
)
}
module.exports = { enqueueRecycleablePhoneNumber }
......@@ -4,8 +4,7 @@ const { errors } = require('./common')
const { destroy } = require('./destroy')
const { list } = require('./present')
const { provisionN } = require('./provision')
const { recycle } = require('./recycle')
const { enqueueRecycleablePhoneNumber } = require('./enqueueRecycle')
const { requestToRecycle, recycle } = require('./recycle')
const { register, registerAllPurchased, registerAllUnregistered } = require('./register')
const { handleSms } = require('./sms')
const { purchase, purchaseN } = require('./purchase')
......@@ -23,7 +22,7 @@ module.exports = {
provisionN,
purchase,
purchaseN,
enqueueRecycleablePhoneNumber,
requestToRecycle,
recycle,
register,
registerAllPurchased,
......
const channelRepository = require('../../db/repositories/channel')
const eventRepository = require('../../db/repositories/event')
const phoneNumberRepository = require('../../db/repositories/phoneNumber')
const recycleRequestRepository = require('../../db/repositories/recycleRequest')
const common = require('./common')
const { notificationKeys } = require('./common')
const { statuses } = require('../../util')
......@@ -8,6 +9,41 @@ const { defaultLanguage } = require('../../config')
const { eventTypes } = require('../../db/models/event')
const { messagesIn } = require('../../dispatcher/strings/messages')
// (Array<string>) -> Promise<SignalboostStatus>
const requestToRecycle = async phoneNumbers => {
return await Promise.all(
phoneNumbers.map(async phoneNumber => {
try {
const channel = await channelRepository.findDeep(phoneNumber)
if (!channel)
return {
status: statuses.ERROR,
message: `${phoneNumber} must be associated with a channel in order to be recycled.`,
}
const { wasCreated } = await recycleRequestRepository.requestToRecycle(phoneNumber)
if (!wasCreated)
return {
status: statuses.ERROR,
message: `${phoneNumber} has already been enqueued for recycling.`,
}
await common.notifyAdmins(channel, 'channelEnqueuedForRecycling')
return {
status: statuses.SUCCESS,
message: `Issued request to recycle ${phoneNumber}.`,
}
} catch (e) {
return {
status: statuses.ERROR,
message: `Database error trying to issue recycle request for ${phoneNumber}.`,
}
}
}),
)
}
// (string) -> SignalboostStatus
const recycle = async phoneNumber => {
const channel = await channelRepository.findDeep(phoneNumber)
......@@ -36,4 +72,4 @@ const recycle = async phoneNumber => {
}
}
module.exports = { recycle }
module.exports = { requestToRecycle, recycle }
import { expect } from 'chai'
import { afterEach, beforeEach, describe, it } from 'mocha'
import sinon from 'sinon'
import signal from '../../../../app/signal/signal'
import channelRepository from '../../../../app/db/repositories/channel'
import { genPhoneNumber } from '../../../support/factories/phoneNumber'
import recycleablePhoneNumberRepository from '../../../../app/db/repositories/recycleablePhoneNumber'
import { enqueueRecycleablePhoneNumber } from '../../../../app/registrar/phoneNumber'
describe('phone number services -- recycle module', () => {
const phoneNumbers = ['+11111111111', '+12222222222']
let db = {}
const sock = {}
let channel = {
phoneNumber: genPhoneNumber(),
name: 'beep boop channel',
}
let findChannelStub,
findRecycleablePhoneNumberStub,
enqueueRecycleablePhoneNumberStub,
getAdminPhoneNumbersStub,
broadcastMessageStub
beforeEach(() => {
findChannelStub = sinon.stub(channelRepository, 'findDeep')
findRecycleablePhoneNumberStub = sinon.stub(
recycleablePhoneNumberRepository,
'findByPhoneNumber',
)
enqueueRecycleablePhoneNumberStub = sinon.stub(recycleablePhoneNumberRepository, 'enqueue')
getAdminPhoneNumbersStub = sinon.stub(channelRepository, 'getAdminPhoneNumbers')
broadcastMessageStub = sinon.stub(signal, 'broadcastMessage')
})
afterEach(() => {
sinon.restore()
})
describe('enqueueing recycleable phone numbers', () => {
describe('when a phone number does not belong to a valid channel', () => {
beforeEach(async () => {
findChannelStub.returns(Promise.resolve(null))
})
it('returns an ERROR status and message', async () => {
const response = await enqueueRecycleablePhoneNumber({
phoneNumbers: phoneNumbers.join(','),
})
expect(response).to.eql([
{
message: `${
phoneNumbers[0]
} must be associated with a channel in order to be recycled.`,
status: 'ERROR',
},
{
message: `${
phoneNumbers[1]
} must be associated with a channel in order to be recycled.`,
status: 'ERROR',
},
])
})
})
describe('when the phone number has already been enqueued for recycling', () => {
beforeEach(() => {
// findChannelStub.returns(channel)
findRecycleablePhoneNumberStub.returns({})
})
it('returns an ERROR status and message', async () => {
const response = await enqueueRecycleablePhoneNumber({
phoneNumbers: channel.phoneNumber,
})
expect(response).to.eql({
status: 'ERROR',
message: `${channel.phoneNumber} has already been enqueued for recycling.`,
})
})
})
describe('when the phone number belongs to a valid channel', () => {
beforeEach(() => {
findChannelStub.returns(Promise.resolve(channel))
})
it('returns a SUCCESS status and message', async () => {
const response = await enqueueRecycleablePhoneNumber({
phoneNumbers: phoneNumbers.join(','),
})
expect(enqueueRecycleablePhoneNumberStub.callCount).to.eql(1)
})
it('notifies the channel admins that their channel will be recycled soon', async () => {
const response = await enqueueRecycleablePhoneNumber({
phoneNumbers: phoneNumbers.join(','),
})
expect(broadcastMessageStub.callCount).to.eql(1)
})
})
describe('when updating the DB throws an error', () => {
beforeEach(() => {
findChannelStub.callsFake(() => {
throw 'DB err'
})
})
it('returns an ERROR status and message', async () => {
const response = await enqueueRecycleablePhoneNumber({
phoneNumbers: phoneNumbers[0],
})
expect(response).to.eql([
{
status: 'ERROR',
message: `There was an error trying to enqueue ${phoneNumbers[0]} for recycling.`,
},
])
})
})
})
})
import { expect } from 'chai'
import { afterEach, beforeEach, describe, it } from 'mocha'
import phoneNumberService, { recycle } from '../../../../app/registrar/phoneNumber'
import phoneNumberService, {
recycle,
requestToRecycle,
} from '../../../../app/registrar/phoneNumber'
import sinon from 'sinon'
import phoneNumberRepository from '../../../../app/db/repositories/phoneNumber'
import common from '../../../../app/registrar/phoneNumber/common'
import channelRepository from '../../../../app/db/repositories/channel'
import eventRepository from '../../../../app/db/repositories/event'
import common from '../../../../app/registrar/phoneNumber/common'
import phoneNumberRepository from '../../../../app/db/repositories/phoneNumber'
import recycleRequestRepository from '../../../../app/db/repositories/recycleRequest'
import { eventTypes } from '../../../../app/db/models/event'
import { genPhoneNumber } from '../../../support/factories/phoneNumber'
import { deepChannelFactory } from '../../../support/factories/channel'
import { channelFactory, deepChannelFactory } from '../../../support/factories/channel'
import { times } from 'lodash'
describe('phone number services -- recycle module', () => {
const phoneNumber = genPhoneNumber()
const phoneNumbers = times(2, genPhoneNumber)
let updatePhoneNumberStub,
findChannelStub,
destroyChannelStub,
logEventStub,
notifyAdminsStub,
notifyMaintainersStub,
notifyMembersStub,
logEventStub
requestToRecycleStub
beforeEach(() => {
updatePhoneNumberStub = sinon.stub(phoneNumberRepository, 'update')
......@@ -26,6 +34,8 @@ describe('phone number services -- recycle module', () => {
logEventStub = sinon.stub(eventRepository, 'log')
notifyMaintainersStub = sinon.stub(common, 'notifyMaintainers')
notifyMembersStub = sinon.stub(common, 'notifyMembers')
notifyAdminsStub = sinon.stub(common, 'notifyAdmins')
requestToRecycleStub = sinon.stub(recycleRequestRepository, 'requestToRecycle')
})
afterEach(() => sinon.restore())
......@@ -35,6 +45,156 @@ describe('phone number services -- recycle module', () => {
Promise.resolve({ phoneNumber, status }),
)
describe('issuing a request to recycle several phone numbers', () => {
describe('when a phone number does not belong to a valid channel', () => {
beforeEach(async () => {
findChannelStub.returns(Promise.resolve(null))
})
it('returns an ERROR status and message', async () => {
expect(await requestToRecycle(phoneNumbers)).to.have.deep.members([
{
status: 'ERROR',
message: `${
phoneNumbers[0]
} must be associated with a channel in order to be recycled.`,
},
{
status: 'ERROR',
message: `${
phoneNumbers[1]
} must be associated with a channel in order to be recycled.`,
},
])
})
})
describe('when the phone number belongs to a valid channel', () => {
beforeEach(() => {
findChannelStub.callsFake(phoneNumber => Promise.resolve(channelFactory({ phoneNumber })))
})
describe('when a recycle request has already been issued for the phone number', () => {
beforeEach(() => {
requestToRecycleStub.returns(Promise.resolve({ wasCreated: false }))
})
it('attempts to issue a recycle request', async () => {
await requestToRecycle(phoneNumbers)
expect(requestToRecycleStub.callCount).to.eql(2)
})
it('returns an ERROR status and message', async () => {
expect(await requestToRecycle(phoneNumbers)).to.have.deep.members([
{
status: 'ERROR',
message: `${phoneNumbers[0]} has already been enqueued for recycling.`,
},
{
status: 'ERROR',
message: `${phoneNumbers[1]} has already been enqueued for recycling.`,
},
])
})
})
describe('when no recycle requests have been issued for any phone numbers', () => {
beforeEach(() => {
requestToRecycleStub.returns(Promise.resolve({ wasCreated: true }))
})
it('returns a SUCCESS status and message', async () => {
expect(await requestToRecycle(phoneNumbers)).to.have.deep.members([
{
status: 'SUCCESS',
message: `Issued request to recycle ${phoneNumbers[0]}.`,
},
{
status: 'SUCCESS',
message: `Issued request to recycle ${phoneNumbers[1]}.`,
},
])
})
it('notifies the channel admins that their channel will be recycled soon', async () => {
await requestToRecycle(phoneNumbers)
notifyAdminsStub
.getCalls()
.map(call => call.args)
.forEach(([channel, notificationKey]) => {
expect(phoneNumbers).to.include(channel.phoneNumber)
expect(notificationKey).to.eql('channelEnqueuedForRecycling')
})
})
})
})
describe('when updating the DB throws an error', () => {
beforeEach(() => findChannelStub.callsFake(() => Promise.reject('DB err')))
it('returns an ERROR status and message', async () => {
const result = await requestToRecycle(phoneNumbers)
expect(result).to.have.deep.members([
{
status: 'ERROR',
message: `Database error trying to issue recycle request for ${phoneNumbers[0]}.`,
},
{
status: 'ERROR',
message: `Database error trying to issue recycle request for ${phoneNumbers[1]}.`,
},
])
})
})
describe('when some requests succeed and others fail', () => {
const _phoneNumbers = times(4, genPhoneNumber)
const createChannelFake = phoneNumber => Promise.resolve(channelFactory({ phoneNumber }))
const requestIssuedFake = () => Promise.resolve({ wasCreated: true })
const requestNotIssuedFake = () => Promise.resolve({ wasCreated: false })
beforeEach(() => {
findChannelStub
.onCall(0)
.callsFake(() => Promise.reject('BOOM!'))
.onCall(1)
.returns(Promise.resolve(null))
.onCall(2)
.callsFake(createChannelFake)
.onCall(3)
.callsFake(createChannelFake)
requestToRecycleStub
.onCall(0)
.callsFake(requestNotIssuedFake)
.onCall(1)
.callsFake(requestIssuedFake)
})
it('returns different results for each phone number', async () => {
expect(await requestToRecycle(_phoneNumbers)).to.eql([
{
status: 'ERROR',
message: `Database error trying to issue recycle request for ${_phoneNumbers[0]}.`,
},
{
status: 'ERROR',
message: `${
_phoneNumbers[1]
} must be associated with a channel in order to be recycled.`,
},
{
status: 'ERROR',
message: `${_phoneNumbers[2]} has already been enqueued for recycling.`,
},
{
status: 'SUCCESS',
message: `Issued request to recycle ${_phoneNumbers[3]}.`,
},
])
})
})
})
describe('recycling a phone number', () => {
describe('when the phone number does not belong to a valid channel', () => {
beforeEach(async () => {
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment