Skip to content

Commit fe3b57e

Browse files
authored
add tax form processor (#2)
* add tax form processor * fix tax form trait name
1 parent ec0fa7e commit fe3b57e

File tree

8 files changed

+281
-2
lines changed

8 files changed

+281
-2
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ The following parameters can be set in config files or in env variables:
2424
- `STANDARD_TERMS_ID`: The Topcoder standard terms id
2525
- `NDA_TERMS_ID`: The NDA terms id
2626
- `TERMS_USER_AGREEMENT_TOPIC`: The kafka topic on which the processor will listen to terms agreement events
27+
- `USER_TAXFORM_UPDATE_TOPIC`: The Kafka topic to which to listen to user tax form updated events
2728
- `auth0.AUTH0_URL`: Auth0 URL, used to get TC M2M token
2829
- `auth0.AUTH0_AUDIENCE`: Auth0 audience, used to get TC M2M token
2930
- `auth0.AUTH0_CLIENT_ID`: Auth0 client id, used to get TC M2M token

Verification.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ The following verification steps expect that the member-api is properly working
2727
- Malformed JSON message:
2828
`{"topic":"terms.notification.user.invalid","originator":"onboarding-api","timestamp":"2021-09-14T00:00:00.000Z","mime-type":"application/json","payload":{"userId":251280,"termsOfUseId":"0dedac8f-5a1a-4fe7-001f-e1d04dc65b7d","legacyId":123456,"created":"2021-09-14T00:00:`
2929

30+
## Verify tax form processor service
31+
32+
`docker exec -it onboarding-checklist-processor_kafka /opt/kafka/bin/kafka-console-producer.sh --broker-list localhost:9092 --topic terms.notification.user.taxform.updated`
33+
34+
- message for user tax form updated
35+
`{"topic":"terms.notification.user.taxform.updated","originator":"onboarding-api","timestamp":"2021-09-14T00:00:00.000Z","mime-type":"application/json","payload":{"userId":251280,"taxForm":"W-9(TopCoder)","Handle":"denis","created":"2021-09-14T00:00:00.000Z"}}`
36+
3037

3138
# Unit tests
3239
To run unit tests, execute the following command `npm run test`

config/default.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ module.exports = {
2222

2323
topics: {
2424
// The Kafka topic to which to listen to user terms agreement events
25-
TERMS_USER_AGREEMENT_TOPIC: process.env.TERMS_USER_AGREEMENT_TOPIC || 'terms.notification.user.agreed'
25+
TERMS_USER_AGREEMENT_TOPIC: process.env.TERMS_USER_AGREEMENT_TOPIC || 'terms.notification.user.agreed',
26+
// The Kafka topic to which to listen to user tax form updated events
27+
USER_TAXFORM_UPDATE_TOPIC: process.env.USER_TAXFORM_UPDATE_TOPIC || 'terms.notification.user.taxform.updated'
2628
},
2729

2830
auth0: {

src/app.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const healthcheck = require('topcoder-healthcheck-dropin')
99
const logger = require('./common/logger')
1010
const helper = require('./common/helper')
1111
const TermsAgreementProcessorService = require('./services/TermsAgreementProcessorService')
12+
const TaxFormProcessorService = require('./services/TaxFormProcessorService')
1213
const Mutex = require('async-mutex').Mutex
1314
const events = require('events')
1415

@@ -23,7 +24,8 @@ const localLogger = {
2324
}
2425

2526
const topicServiceMapping = {
26-
[config.topics.TERMS_USER_AGREEMENT_TOPIC]: TermsAgreementProcessorService.processMessage
27+
[config.topics.TERMS_USER_AGREEMENT_TOPIC]: TermsAgreementProcessorService.processMessage,
28+
[config.topics.USER_TAXFORM_UPDATE_TOPIC]: TaxFormProcessorService.processMessage
2729
}
2830

2931
// Start kafka consumer

src/common/constants.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ module.exports = {
1515
// The property name to use to store the nda terms in traits body
1616
NDA_TERMS_TRAIT_PROPERTY_NAME: 'nda_terms',
1717

18+
// The property name to use to store the tax form submitted in traits body
19+
TAX_FORM_TRAIT_PROPERTY_NAME: 'tax_form_submitted',
20+
TAX_FORM_TRAIT_PROPERTY_NAME_MAP: { 'w9_tax_form_submitted': 'W-9', 'w8ben_tax_form_submitted': 'W-8BEN' },
21+
1822
CHECKLIST_STATUS: {
1923
COMPLETED: 'completed'
2024
},
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/**
2+
* This service processes the events fired when users agree to Standard Topcoder terms and NDA terms
3+
*/
4+
5+
const Joi = require('@hapi/joi')
6+
const _ = require('lodash')
7+
const logger = require('../common/logger')
8+
const helper = require('../common/helper')
9+
const constants = require('../common/constants')
10+
11+
/**
12+
* Process the terms agreement event message
13+
* @param {Object} message the kafka message
14+
*/
15+
async function processMessage (message) {
16+
logger.info(`Process message of taxForm ${message.payload.taxForm}, userId ${message.payload.userId}, handle ${message.payload.Handle}`)
17+
// Get the user handle from members api
18+
const handle = _.get(message.payload, 'Handle')
19+
const taxForm = _.get(message.payload, 'taxForm')
20+
21+
// Get the member Onboarding Checklist traits
22+
const onboardingChecklistTraits = await helper.getMemberTraits(handle, constants.ONBOARDING_CHECKLIST_TRAIT_ID)
23+
24+
// Initialize the terms traits data object
25+
const termsTraitsData = {
26+
status: constants.CHECKLIST_STATUS.COMPLETED,
27+
message: constants.CHECKLIST_MESSAGE.SUCCESS,
28+
date: new Date().getTime()
29+
}
30+
31+
const traitsBodyPropertyName = _.findKey(constants.TAX_FORM_TRAIT_PROPERTY_NAME_MAP, v => _.startsWith(taxForm, v)) || constants.TAX_FORM_TRAIT_PROPERTY_NAME
32+
if (onboardingChecklistTraits.length === 0) {
33+
const body = [{
34+
categoryName: constants.ONBOARDING_CHECKLIST_CATEGORY_NAME,
35+
traitId: constants.ONBOARDING_CHECKLIST_TRAIT_ID,
36+
traits: {
37+
data: [{
38+
[traitsBodyPropertyName]: termsTraitsData
39+
}]
40+
}
41+
}]
42+
await helper.saveMemberTraits(handle, body, true)
43+
} else {
44+
// Onboarding checklist traits already exists for the member
45+
// An update of the trait should be performed
46+
47+
// Update the currently processed terms property in the request body
48+
onboardingChecklistTraits[0].traits.data[0][traitsBodyPropertyName] = termsTraitsData
49+
await helper.saveMemberTraits(handle, onboardingChecklistTraits, false)
50+
}
51+
52+
logger.info(`Successfully Processed message of taxForm ${taxForm}, userId ${message.payload.userId}, handle ${handle}`)
53+
}
54+
55+
processMessage.schema = {
56+
message: Joi.object()
57+
.keys({
58+
topic: Joi.string().required(),
59+
originator: Joi.string().required(),
60+
timestamp: Joi.date().required(),
61+
'mime-type': Joi.string().required(),
62+
payload: Joi.object()
63+
.keys({
64+
userId: Joi.alternatives(Joi.string(), Joi.positiveId()).required(),
65+
taxForm: Joi.string().required(),
66+
Handle: Joi.string().required(),
67+
created: Joi.date()
68+
})
69+
.required()
70+
})
71+
.required()
72+
}
73+
74+
module.exports = {
75+
processMessage
76+
}
77+
78+
logger.buildService(module.exports, 'TaxFormProcessorService')

test/common/taxFormTestData.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* This file defines common data used in tests.
3+
*/
4+
5+
const denisUserId = '251280'
6+
const upbeatUserHandle = 'upbeat'
7+
8+
const createTaxFormTraitTestMessage = {
9+
'topic': 'terms.notification.user.agreed',
10+
'originator': 'onboarding-api',
11+
'timestamp': '2021-09-14T00:00:00.000Z',
12+
'mime-type': 'application/json',
13+
'payload': {
14+
'userId': denisUserId,
15+
'taxForm': 'W-9(TopCoder)',
16+
'Handle': 'denis',
17+
'created': '2021-09-14T00:00:00.000Z'
18+
}
19+
}
20+
21+
const requiredFields = ['originator', 'timestamp', 'mime-type', 'topic', 'payload.userId', 'payload.taxForm', 'payload.Handle']
22+
23+
const stringFields = ['topic', 'originator', 'mime-type', 'payload.taxForm', 'payload.Handle']
24+
25+
const dateFields = ['timestamp', 'payload.created']
26+
27+
module.exports = {
28+
createTaxFormTraitTestMessage,
29+
requiredFields,
30+
stringFields,
31+
dateFields,
32+
denisUserId,
33+
upbeatUserHandle
34+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/**
2+
* Unit tests for Onboarding Checklist Tax Form updated processor service
3+
*/
4+
5+
process.env.NODE_ENV = 'test'
6+
7+
require('../../src/bootstrap')
8+
const _ = require('lodash')
9+
const should = require('should')
10+
const logger = require('../../src/common/logger')
11+
const service = require('../../src/services/TaxFormProcessorService')
12+
const {
13+
createTaxFormTraitTestMessage,
14+
requiredFields,
15+
stringFields,
16+
dateFields,
17+
upbeatUserHandle
18+
} = require('../common/taxFormTestData')
19+
20+
describe('Topcoder Onboarding Checklist - Tax Form Processor Service Unit Tests', () => {
21+
let infoLogs = []
22+
let errorLogs = []
23+
let debugLogs = []
24+
const info = logger.info
25+
const error = logger.error
26+
const debug = logger.debug
27+
28+
/**
29+
* Assert validation error
30+
* @param err the error
31+
* @param message the message
32+
*/
33+
function assertValidationError (err, message) {
34+
err.isJoi.should.be.true()
35+
should.equal(err.name, 'ValidationError')
36+
err.details.map(x => x.message).should.containEql(message)
37+
errorLogs.should.not.be.empty()
38+
}
39+
40+
/**
41+
* Assert there is given info message
42+
* @param message the message
43+
*/
44+
function assertInfoMessage (message) {
45+
infoLogs.should.not.be.empty()
46+
infoLogs.some(x => String(x).includes(message)).should.be.true()
47+
}
48+
49+
before(async () => {
50+
// inject logger with log collector
51+
logger.info = (message) => {
52+
infoLogs.push(message)
53+
info(message)
54+
}
55+
logger.error = (message) => {
56+
errorLogs.push(message)
57+
error(message)
58+
}
59+
logger.debug = (message) => {
60+
debugLogs.push(message)
61+
debug(message)
62+
}
63+
})
64+
65+
after(async () => {
66+
// restore logger
67+
logger.error = error
68+
logger.info = info
69+
logger.debug = debug
70+
})
71+
72+
beforeEach(() => {
73+
// clear logs
74+
infoLogs = []
75+
errorLogs = []
76+
debugLogs = []
77+
})
78+
79+
for (const requiredField of requiredFields) {
80+
it(`test process message with invalid parameters, required field ${requiredField} is missing`, async () => {
81+
let message = _.cloneDeep(createTaxFormTraitTestMessage)
82+
message = _.omit(message, requiredField)
83+
try {
84+
await service.processMessage(message)
85+
} catch (err) {
86+
assertValidationError(err, `"${_.last(requiredField.split('.'))}" is required`)
87+
return
88+
}
89+
throw new Error('should not throw error here')
90+
})
91+
}
92+
93+
for (const stringField of stringFields) {
94+
it(`test process message with invalid parameters, invalid string type field ${stringField}`, async () => {
95+
const message = _.cloneDeep(createTaxFormTraitTestMessage)
96+
_.set(message, stringField, 123)
97+
try {
98+
await service.processMessage(message)
99+
} catch (err) {
100+
assertValidationError(err, `"${_.last(stringField.split('.'))}" must be a string`)
101+
return
102+
}
103+
throw new Error('should not throw error here')
104+
})
105+
106+
it(`test process message with invalid parameters, empty string field ${stringField}`, async () => {
107+
const message = _.cloneDeep(createTaxFormTraitTestMessage)
108+
_.set(message, stringField, '')
109+
try {
110+
await service.processMessage(message)
111+
} catch (err) {
112+
assertValidationError(err, `"${_.last(stringField.split('.'))}" is not allowed to be empty`)
113+
return
114+
}
115+
throw new Error('should not throw error here')
116+
})
117+
}
118+
119+
for (const dateField of dateFields) {
120+
it(`test process message with invalid parameters, invalid date type field ${dateField}`, async () => {
121+
const message = _.cloneDeep(createTaxFormTraitTestMessage)
122+
_.set(message, dateField, 'abc')
123+
try {
124+
await service.processMessage(message)
125+
} catch (err) {
126+
assertValidationError(err,
127+
`"${_.last(dateField.split('.'))}" must be a number of milliseconds or valid date string`)
128+
return
129+
}
130+
throw new Error('should not throw error here')
131+
})
132+
}
133+
134+
it(`test process message - onboarding checklist does not already exist for the member`, async () => {
135+
const message = _.cloneDeep(createTaxFormTraitTestMessage)
136+
await service.processMessage(message)
137+
138+
assertInfoMessage(`Process message of taxForm ${message.payload.taxForm}, userId ${message.payload.userId}, handle ${message.payload.Handle}`)
139+
assertInfoMessage(`Successfully Processed message of taxForm ${message.payload.taxForm}, userId ${message.payload.userId}, handle ${message.payload.Handle}`)
140+
})
141+
142+
it(`test process message - onboarding checklist already exist for the member`, async () => {
143+
const message = _.cloneDeep(createTaxFormTraitTestMessage)
144+
_.set(message, 'payload.Handle', upbeatUserHandle)
145+
146+
await service.processMessage(message)
147+
148+
assertInfoMessage(`Process message of taxForm ${message.payload.taxForm}, userId ${message.payload.userId}, handle ${message.payload.Handle}`)
149+
assertInfoMessage(`Successfully Processed message of taxForm ${message.payload.taxForm}, userId ${message.payload.userId}, handle ${message.payload.Handle}`)
150+
})
151+
})

0 commit comments

Comments
 (0)