diff --git a/.gitignore b/.gitignore index 7d71a33..f00801a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ coverage .DS_Store .env api.env +.eslintrc.y*ml diff --git a/README.md b/README.md index 0501dec..045684f 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,11 @@ The following parameters can be set in config files or in env variables: - `topics.TAAS_RESOURCE_BOOKING_CREATE_TOPIC`: the create resource booking entity Kafka message topic - `topics.TAAS_RESOURCE_BOOKING_UPDATE_TOPIC`: the update resource booking entity Kafka message topic - `topics.TAAS_RESOURCE_BOOKING_DELETE_TOPIC`: the delete resource booking entity Kafka message topic +- `topics.TAAS_WORK_PERIOD_CREATE_TOPIC`: the create work period entity Kafka message topic +- `topics.TAAS_WORK_PERIOD_UPDATE_TOPIC`: the update work period entity Kafka message topic +- `topics.TAAS_WORK_PERIOD_DELETE_TOPIC`: the delete work period entity Kafka message topic +- `topics.TAAS_WORK_PERIOD_PAYMENT_CREATE_TOPIC`: the create work period payment entity Kafka message topic +- `topics.TAAS_WORK_PERIOD_PAYMENT_UPDATE_TOPIC`: the update work period payment entity Kafka message topic - `esConfig.HOST`: Elasticsearch host - `esConfig.AWS_REGION`: The Amazon region to use when using AWS Elasticsearch service - `esConfig.ELASTICCLOUD.id`: The elastic cloud id, if your elasticsearch instance is hosted on elastic cloud. DO NOT provide a value for ES_HOST if you are using this @@ -38,6 +43,7 @@ The following parameters can be set in config files or in env variables: - `esConfig.ES_INDEX_JOB`: the index name for job - `esConfig.ES_INDEX_JOB_CANDIDATE`: the index name for job candidate - `esConfig.ES_INDEX_RESOURCE_BOOKING`: the index name for resource booking +- `esConfig.ES_INDEX_WORK_PERIOD`: the index name for work period - `auth0.AUTH0_URL`: Auth0 URL, used to get TC M2M token - `auth0.AUTH0_AUDIENCE`: Auth0 audience, used to get TC M2M token diff --git a/VERIFICATION.md b/VERIFICATION.md index d1d48d3..5410bcf 100644 --- a/VERIFICATION.md +++ b/VERIFICATION.md @@ -2,7 +2,7 @@ ## Create documents in ES -- Run the following commands to create `Job`, `JobCandidate` and `ResourceBooking` documents in ES. +- Run the following commands to create `Job`, `JobCandidate`, `ResourceBooking`, `WorkPeriod`, `WorkPeriodPayment` documents in ES. ``` bash # for Job @@ -11,12 +11,16 @@ docker exec -i taas-es-processor_kafka /opt/kafka/bin/kafka-console-producer.sh --broker-list localhost:9092 --topic taas.jobcandidate.create < test/messages/taas.jobcandidate.create.event.json # for ResourceBooking docker exec -i taas-es-processor_kafka /opt/kafka/bin/kafka-console-producer.sh --broker-list localhost:9092 --topic taas.resourcebooking.create < test/messages/taas.resourcebooking.create.event.json + # for WorkPeriod + docker exec -i taas-es-processor_kafka /opt/kafka/bin/kafka-console-producer.sh --broker-list localhost:9092 --topic taas.workperiod.create < test/messages/taas.workperiod.create.event.json + # for WorkPeriodPayment + docker exec -i taas-es-processor_kafka /opt/kafka/bin/kafka-console-producer.sh --broker-list localhost:9092 --topic taas.workperiodpayment.create < test/messages/taas.workperiodpayment.create.event.json ``` - Run `npm run view-data ` to see if documents were created. ## Update documents in ES -- Run the following commands to update `Job`, `JobCandidate` and `ResourceBooking` documents in ES. +- Run the following commands to update `Job`, `JobCandidate`, `ResourceBooking`, `WorkPeriod`, `WorkPeriodPayment` documents in ES. ``` bash # for Job @@ -25,12 +29,16 @@ docker exec -i taas-es-processor_kafka /opt/kafka/bin/kafka-console-producer.sh --broker-list localhost:9092 --topic taas.jobcandidate.update < test/messages/taas.jobcandidate.update.event.json # for ResourceBooking docker exec -i taas-es-processor_kafka /opt/kafka/bin/kafka-console-producer.sh --broker-list localhost:9092 --topic taas.resourcebooking.update < test/messages/taas.resourcebooking.update.event.json + # for WorkPeriod + docker exec -i taas-es-processor_kafka /opt/kafka/bin/kafka-console-producer.sh --broker-list localhost:9092 --topic taas.workperiod.update < test/messages/taas.workperiod.update.event.json + # for WorkPeriodPayment + docker exec -i taas-es-processor_kafka /opt/kafka/bin/kafka-console-producer.sh --broker-list localhost:9092 --topic taas.workperiodpayment.update < test/messages/taas.workperiodpayment.update.event.json ``` - Run `npm run view-data ` to see if documents were updated. ## Delete documents in ES -- Run the following commands to delete `Job`, `JobCandidate` and `ResourceBooking` documents in ES. +- Run the following commands to delete `Job`, `JobCandidate`, `ResourceBooking`, `WorkPeriod` documents in ES. ``` bash # for Job @@ -39,6 +47,8 @@ docker exec -i taas-es-processor_kafka /opt/kafka/bin/kafka-console-producer.sh --broker-list localhost:9092 --topic taas.jobcandidate.delete < test/messages/taas.jobcandidate.delete.event.json # for ResourceBooking docker exec -i taas-es-processor_kafka /opt/kafka/bin/kafka-console-producer.sh --broker-list localhost:9092 --topic taas.resourcebooking.delete < test/messages/taas.resourcebooking.delete.event.json + # for WorkPeriod + docker exec -i taas-es-processor_kafka /opt/kafka/bin/kafka-console-producer.sh --broker-list localhost:9092 --topic taas.workperiod.delete < test/messages/taas.workperiod.delete.event.json ``` - Run `npm run view-data ` to see if documents were deleted. diff --git a/config/default.js b/config/default.js index c09c3e5..7a8dbf8 100644 --- a/config/default.js +++ b/config/default.js @@ -16,6 +16,7 @@ module.exports = { KAFKA_GROUP_ID: process.env.KAFKA_GROUP_ID || 'taas-es-processor', topics: { + // topics for job service TAAS_JOB_CREATE_TOPIC: process.env.TAAS_JOB_CREATE_TOPIC || 'taas.job.create', TAAS_JOB_UPDATE_TOPIC: process.env.TAAS_JOB_UPDATE_TOPIC || 'taas.job.update', TAAS_JOB_DELETE_TOPIC: process.env.TAAS_JOB_DELETE_TOPIC || 'taas.job.delete', @@ -23,10 +24,17 @@ module.exports = { TAAS_JOB_CANDIDATE_CREATE_TOPIC: process.env.TAAS_JOB_CANDIDATE_CREATE_TOPIC || 'taas.jobcandidate.create', TAAS_JOB_CANDIDATE_UPDATE_TOPIC: process.env.TAAS_JOB_CANDIDATE_UPDATE_TOPIC || 'taas.jobcandidate.update', TAAS_JOB_CANDIDATE_DELETE_TOPIC: process.env.TAAS_JOB_CANDIDATE_DELETE_TOPIC || 'taas.jobcandidate.delete', - // topics for job service + // topics for resource booking service TAAS_RESOURCE_BOOKING_CREATE_TOPIC: process.env.TAAS_RESOURCE_BOOKING_CREATE_TOPIC || 'taas.resourcebooking.create', TAAS_RESOURCE_BOOKING_UPDATE_TOPIC: process.env.TAAS_RESOURCE_BOOKING_UPDATE_TOPIC || 'taas.resourcebooking.update', - TAAS_RESOURCE_BOOKING_DELETE_TOPIC: process.env.TAAS_RESOURCE_BOOKING_DELETE_TOPIC || 'taas.resourcebooking.delete' + TAAS_RESOURCE_BOOKING_DELETE_TOPIC: process.env.TAAS_RESOURCE_BOOKING_DELETE_TOPIC || 'taas.resourcebooking.delete', + // topics for work period service + TAAS_WORK_PERIOD_CREATE_TOPIC: process.env.TAAS_WORK_PERIOD_CREATE_TOPIC || 'taas.workperiod.create', + TAAS_WORK_PERIOD_UPDATE_TOPIC: process.env.TAAS_WORK_PERIOD_UPDATE_TOPIC || 'taas.workperiod.update', + TAAS_WORK_PERIOD_DELETE_TOPIC: process.env.TAAS_WORK_PERIOD_DELETE_TOPIC || 'taas.workperiod.delete', + // topics for work period payment service + TAAS_WORK_PERIOD_PAYMENT_CREATE_TOPIC: process.env.TAAS_WORK_PERIOD_PAYMENT_CREATE_TOPIC || 'taas.workperiodpayment.create', + TAAS_WORK_PERIOD_PAYMENT_UPDATE_TOPIC: process.env.TAAS_WORK_PERIOD_PAYMENT_UPDATE_TOPIC || 'taas.workperiodpayment.update' }, esConfig: { @@ -42,7 +50,8 @@ module.exports = { ES_INDEX_JOB: process.env.ES_INDEX_JOB || 'job', ES_INDEX_JOB_CANDIDATE: process.env.ES_INDEX_JOB_CANDIDATE || 'job_candidate', - ES_INDEX_RESOURCE_BOOKING: process.env.ES_INDEX_RESOURCE_BOOKING || 'resource_booking' + ES_INDEX_RESOURCE_BOOKING: process.env.ES_INDEX_RESOURCE_BOOKING || 'resource_booking', + ES_INDEX_WORK_PERIOD: process.env.ES_INDEX_WORK_PERIOD || 'work_period' }, auth0: { diff --git a/config/test.js b/config/test.js new file mode 100644 index 0000000..c462fb0 --- /dev/null +++ b/config/test.js @@ -0,0 +1,10 @@ +/** + * The default configuration file. + */ + +module.exports = { + zapier: { + ZAPIER_SWITCH: process.env.ZAPIER_SWITCH || 'ON', + ZAPIER_JOB_CANDIDATE_SWITCH: process.env.ZAPIER_JOB_CANDIDATE_SWITCH || 'ON' + } +} diff --git a/local/docker-compose.yml b/local/docker-compose.yml index 5d2d803..35e9486 100644 --- a/local/docker-compose.yml +++ b/local/docker-compose.yml @@ -12,7 +12,7 @@ services: - "9092:9092" environment: KAFKA_ADVERTISED_HOST_NAME: localhost - KAFKA_CREATE_TOPICS: "taas.job.create:1:1,taas.jobcandidate.create:1:1,taas.resourcebooking.create:1:1,taas.job.update:1:1,taas.jobcandidate.update:1:1,taas.resourcebooking.update:1:1,taas.job.delete:1:1,taas.jobcandidate.delete:1:1,taas.resourcebooking.delete:1:1" + KAFKA_CREATE_TOPICS: "taas.job.create:1:1,taas.jobcandidate.create:1:1,taas.resourcebooking.create:1:1,taas.workperiod.create:1:1,taas.workperiodpayment.create:1:1,taas.job.update:1:1,taas.jobcandidate.update:1:1,taas.resourcebooking.update:1:1,taas.workperiod.update:1:1,taas.workperiodpayment.update:1:1,taas.job.delete:1:1,taas.jobcandidate.delete:1:1,taas.resourcebooking.delete:1:1,taas.workperiod.delete:1:1" KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 esearch: image: elasticsearch:7.7.1 diff --git a/package-lock.json b/package-lock.json index 91412d7..6a4fb26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -216,6 +216,41 @@ } } }, + "@sinonjs/commons": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", + "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.0.4.tgz", + "integrity": "sha512-fW3SzjLF0sjI0x1Opc7cUG4J/Nr4U0TXPNnKNAgrxA4xXsQNk6nypZK0yJg5FNw5cCo2yC/ZMdaVhDTKeeF6zg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "@sinonjs/samsam": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.0.1.tgz", + "integrity": "sha512-zJ+xzDBMETj/kFkagaZBG4G8e80Et182r6xpCzpubS7cavdTLwBKtCU3sgmPvZDC0u41gd87atcoUxcmiamBgw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.6.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, "@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -962,6 +997,58 @@ "safe-buffer": "^5.0.1" } }, + "cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.1" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -2635,6 +2722,12 @@ "object.assign": "^4.1.0" } }, + "just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true + }, "jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -2759,6 +2852,12 @@ "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", "dev": true }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true + }, "lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -3246,6 +3345,45 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "nise": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.0.1.tgz", + "integrity": "sha512-U6qdfulSDpEgx3WSoeMlKZ6hGaTMKtyW7CY3bjj0MK3uzHvmugyteB2zyHQRRvi5I91oErSUR595s7htS6IYRQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0", + "@sinonjs/fake-timers": "^6.0.0", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "@sinonjs/fake-timers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", + "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + } + } + }, "no-kafka": { "version": "3.4.3", "resolved": "https://registry.npm.taobao.org/no-kafka/download/no-kafka-3.4.3.tgz", @@ -4270,6 +4408,43 @@ "is-arrayish": "^0.3.1" } }, + "sinon": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-10.0.1.tgz", + "integrity": "sha512-1rf86mvW4Mt7JitEIgmNaLXaWnrWd/UrVKZZlL+kbeOujXVf9fmC4kQEQ/YeHoiIA23PLNngYWK+dngIx/AumA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.8.1", + "@sinonjs/fake-timers": "^7.0.4", + "@sinonjs/samsam": "^6.0.1", + "diff": "^4.0.2", + "nise": "^5.0.1", + "supports-color": "^7.1.0" + }, + "dependencies": { + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "slice-ansi": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", @@ -4948,6 +5123,12 @@ "prelude-ls": "~1.1.2" } }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", diff --git a/package.json b/package.json index 7798acd..73f7cc9 100644 --- a/package.json +++ b/package.json @@ -10,19 +10,21 @@ "create-index": "node src/scripts/createIndex.js", "delete-index": "node src/scripts/deleteIndex.js", "view-data": "node src/scripts/view-data.js", - "test": "mocha test/unit/test.js --require test/unit/prepare.js --timeout 20000 --exit", - "test:cov": "nyc --reporter=html --reporter=text mocha test/unit/test.js --require test/unit/prepare.js --timeout 20000 --exit", - "e2e": "mocha test/e2e/test.js --timeout 20000 --exit", - "e2e:cov": "nyc --reporter=html --reporter=text mocha test/e2e/test.js --timeout 20000 --exit" + "test": "cross-env NODE_ENV=test mocha test/unit/test.js --require test/unit/prepare.js --timeout 20000 --exit", + "test:cov": "cross-env NODE_ENV=test nyc --reporter=html --reporter=text mocha test/unit/test.js --require test/unit/prepare.js --timeout 20000 --exit", + "e2e": "cross-env NODE_ENV=test mocha test/e2e/test.js --timeout 20000 --exit", + "e2e:cov": "cross-env NODE_ENV=test nyc --reporter=html --reporter=text mocha test/e2e/test.js --timeout 20000 --exit" }, "author": "TCSCODER", "license": "none", "devDependencies": { + "cross-env": "^7.0.3", "mocha": "^7.1.2", "mocha-prepare": "^0.1.0", "nock": "^12.0.3", "nyc": "^14.1.1", "should": "^13.2.3", + "sinon": "^10.0.1", "standard": "^12.0.1", "stringcase": "^4.3.1", "superagent": "^5.1.0" diff --git a/src/app.js b/src/app.js index 7414dc2..c38c03d 100644 --- a/src/app.js +++ b/src/app.js @@ -12,6 +12,8 @@ const helper = require('./common/helper') const JobProcessorService = require('./services/JobProcessorService') const JobCandidateProcessorService = require('./services/JobCandidateProcessorService') const ResourceBookingProcessorService = require('./services/ResourceBookingProcessorService') +const WorkPeriodProcessorService = require('./services/WorkPeriodProcessorService') +const WorkPeriodPaymentProcessorService = require('./services/WorkPeriodPaymentProcessorService') const Mutex = require('async-mutex').Mutex const events = require('events') @@ -38,7 +40,14 @@ const topicServiceMapping = { // resource booking [config.topics.TAAS_RESOURCE_BOOKING_CREATE_TOPIC]: ResourceBookingProcessorService.processCreate, [config.topics.TAAS_RESOURCE_BOOKING_UPDATE_TOPIC]: ResourceBookingProcessorService.processUpdate, - [config.topics.TAAS_RESOURCE_BOOKING_DELETE_TOPIC]: ResourceBookingProcessorService.processDelete + [config.topics.TAAS_RESOURCE_BOOKING_DELETE_TOPIC]: ResourceBookingProcessorService.processDelete, + // work period + [config.topics.TAAS_WORK_PERIOD_CREATE_TOPIC]: WorkPeriodProcessorService.processCreate, + [config.topics.TAAS_WORK_PERIOD_UPDATE_TOPIC]: WorkPeriodProcessorService.processUpdate, + [config.topics.TAAS_WORK_PERIOD_DELETE_TOPIC]: WorkPeriodProcessorService.processDelete, + // work period payment + [config.topics.TAAS_WORK_PERIOD_PAYMENT_CREATE_TOPIC]: WorkPeriodPaymentProcessorService.processCreate, + [config.topics.TAAS_WORK_PERIOD_PAYMENT_UPDATE_TOPIC]: WorkPeriodPaymentProcessorService.processUpdate } // Start kafka consumer diff --git a/src/bootstrap.js b/src/bootstrap.js index 5648bbc..794ab83 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -6,9 +6,12 @@ global.Promise = require('bluebird') Joi.rateType = () => Joi.string().valid('hourly', 'daily', 'weekly', 'monthly') Joi.jobStatus = () => Joi.string().valid('sourcing', 'in-review', 'assigned', 'closed', 'cancelled') -Joi.jobCandidateStatus = () => Joi.string().valid('open', 'selected', 'shortlist', 'rejected', 'cancelled', 'interview') +Joi.resourceBookingStatus = () => Joi.string().valid('assigned', 'closed', 'cancelled') +Joi.jobCandidateStatus = () => Joi.string().valid('open', 'selected', 'shortlist', 'rejected', 'cancelled', 'interview', 'topcoder-rejected') Joi.workload = () => Joi.string().valid('full-time', 'fractional') Joi.title = () => Joi.string().max(128) +Joi.paymentStatus = () => Joi.string().valid('pending', 'partially-completed', 'completed', 'cancelled') +Joi.workPeriodPaymentStatus = () => Joi.string().valid('completed', 'cancelled') // Empty string is not allowed by Joi by default and must be enabled with allow(''). // See https://joi.dev/api/?v=17.3.0#string fro details why it's like this. // In many cases we would like to allow empty string to make it easier to create UI for editing data. diff --git a/src/common/helper.js b/src/common/helper.js index d06b1a1..281b335 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -109,6 +109,22 @@ function getESClient () { } } + // get document or catch not found error + esClient.getExtra = async function (data) { + let doc + + try { + doc = await esClient.getSource(data) + } catch (err) { + if (err.statusCode === 404) { + throw new Error(`id: ${data.id} "${data.index}" not found`) + } + throw err + } + + return doc + } + // delete document or catch not found error esClient.deleteExtra = async function (data) { try { diff --git a/src/scripts/createIndex.js b/src/scripts/createIndex.js index c6986df..c4601ee 100644 --- a/src/scripts/createIndex.js +++ b/src/scripts/createIndex.js @@ -63,11 +63,49 @@ async function createIndex () { userId: { type: 'keyword' }, jobId: { type: 'keyword' }, status: { type: 'keyword' }, - startDate: { type: 'date' }, - endDate: { type: 'date' }, + startDate: { type: 'date', format: 'yyyy-MM-dd' }, + endDate: { type: 'date', format: 'yyyy-MM-dd' }, memberRate: { type: 'float' }, customerRate: { type: 'float' }, rateType: { type: 'keyword' }, + billingAccountId: { type: 'integer' }, + createdAt: { type: 'date' }, + createdBy: { type: 'keyword' }, + updatedAt: { type: 'date' }, + updatedBy: { type: 'keyword' } + } + } + } + }, + { + index: config.get('esConfig.ES_INDEX_WORK_PERIOD'), + body: { + mappings: { + properties: { + resourceBookingId: { type: 'keyword' }, + userHandle: { type: 'keyword' }, + projectId: { type: 'integer' }, + userId: { type: 'keyword' }, + startDate: { type: 'date', format: 'yyyy-MM-dd' }, + endDate: { type: 'date', format: 'yyyy-MM-dd' }, + daysWorked: { type: 'integer' }, + memberRate: { type: 'float' }, + customerRate: { type: 'float' }, + paymentStatus: { type: 'keyword' }, + payments: { + type: 'nested', + properties: { + workPeriodId: { type: 'keyword' }, + challengeId: { type: 'keyword' }, + amount: { type: 'float' }, + status: { type: 'keyword' }, + billingAccountId: { type: 'integer' }, + createdAt: { type: 'date' }, + createdBy: { type: 'keyword' }, + updatedAt: { type: 'date' }, + updatedBy: { type: 'keyword' } + } + }, createdAt: { type: 'date' }, createdBy: { type: 'keyword' }, updatedAt: { type: 'date' }, diff --git a/src/scripts/deleteIndex.js b/src/scripts/deleteIndex.js index 9c3a1bb..69594b4 100644 --- a/src/scripts/deleteIndex.js +++ b/src/scripts/deleteIndex.js @@ -11,7 +11,8 @@ async function deleteIndex () { const esClient = helper.getESClient() const indices = [config.get('esConfig.ES_INDEX_JOB'), config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), - config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] + config.get('esConfig.ES_INDEX_RESOURCE_BOOKING'), + config.get('esConfig.ES_INDEX_WORK_PERIOD')] for (const index of indices) { await esClient.indices.delete({ index diff --git a/src/scripts/view-data.js b/src/scripts/view-data.js index 1db36d5..9c3d0ce 100644 --- a/src/scripts/view-data.js +++ b/src/scripts/view-data.js @@ -11,7 +11,8 @@ const esClient = helper.getESClient() const modelIndexMapping = { Job: 'ES_INDEX_JOB', JobCandidate: 'ES_INDEX_JOB_CANDIDATE', - ResourceBooking: 'ES_INDEX_RESOURCE_BOOKING' + ResourceBooking: 'ES_INDEX_RESOURCE_BOOKING', + WorkPeriod: 'ES_INDEX_WORK_PERIOD' } async function showESData () { diff --git a/src/services/JobCandidateProcessorService.js b/src/services/JobCandidateProcessorService.js index 3d0243c..3eeaaaa 100644 --- a/src/services/JobCandidateProcessorService.js +++ b/src/services/JobCandidateProcessorService.js @@ -20,9 +20,9 @@ const localLogger = { * @param {Object} message the message object * @returns {undefined} */ -async function updateCandidateStatus ({ type, payload }) { - if (!payload.status) { - localLogger.debug({ context: 'updateCandidateStatus', message: 'status not updated' }) +async function updateCandidateStatus ({ type, payload, previousData }) { + if (previousData.status === payload.status) { + localLogger.debug({ context: 'updateCandidateStatus', message: `jobCandidate is already in status: ${payload.status}` }) return } if (!['rejected', 'shortlist'].includes(payload.status)) { @@ -56,13 +56,13 @@ async function updateCandidateStatus ({ type, payload }) { * @param {Object} message the message object * @returns {undefined} */ -async function postMessageToZapier ({ type, payload }) { +async function postMessageToZapier ({ type, payload, previousData }) { if (config.zapier.ZAPIER_JOB_CANDIDATE_SWITCH === constants.Zapier.Switch.OFF) { localLogger.debug({ context: 'postMessageToZapier', message: 'Zapier Switch off via config, no messages sent' }) return } if (type === constants.Zapier.MessageType.JobCandidateUpdate) { - await updateCandidateStatus({ type, payload }) + await updateCandidateStatus({ type, payload, previousData }) return } throw new Error(`unrecognized message type: ${type}`) @@ -113,6 +113,12 @@ processCreate.schema = { */ async function processUpdate (message, transactionId) { const data = message.payload + // save previous data for Zapier logic + // NOTE: ideally if we update Kafka event message to have both: pervious and updated value so we don't have to request it again + const { body: previousData } = await esClient.getExtra({ + index: config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), + id: data.id + }) await esClient.updateExtra({ index: config.get('esConfig.ES_INDEX_JOB_CANDIDATE'), id: data.id, @@ -124,7 +130,8 @@ async function processUpdate (message, transactionId) { }) await postMessageToZapier({ type: constants.Zapier.MessageType.JobCandidateUpdate, - payload: data + payload: data, + previousData }) } diff --git a/src/services/ResourceBookingProcessorService.js b/src/services/ResourceBookingProcessorService.js index 964f4d0..f407b2b 100644 --- a/src/services/ResourceBookingProcessorService.js +++ b/src/services/ResourceBookingProcessorService.js @@ -37,8 +37,8 @@ processCreate.schema = { projectId: Joi.number().integer().required(), userId: Joi.string().uuid().required(), jobId: Joi.string().uuid().allow(null), - startDate: Joi.date().allow(null), - endDate: Joi.date().allow(null), + startDate: Joi.string().regex(/^(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$/).allow(null), + endDate: Joi.string().regex(/^(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$/).allow(null), memberRate: Joi.number().allow(null), customerRate: Joi.number().allow(null), rateType: Joi.rateType().required(), @@ -46,7 +46,8 @@ processCreate.schema = { createdBy: Joi.string().uuid().required(), updatedAt: Joi.date().allow(null), updatedBy: Joi.string().uuid().allow(null), - status: Joi.jobStatus().required() + status: Joi.resourceBookingStatus().required(), + billingAccountId: Joi.number().allow(null) }).required() }).required(), transactionId: Joi.string().required() diff --git a/src/services/WorkPeriodPaymentProcessorService.js b/src/services/WorkPeriodPaymentProcessorService.js new file mode 100644 index 0000000..d336379 --- /dev/null +++ b/src/services/WorkPeriodPaymentProcessorService.js @@ -0,0 +1,138 @@ +/** + * WorkPeriodPayment Processor Service + */ + +const Joi = require('@hapi/joi') +const config = require('config') +const _ = require('lodash') +const logger = require('../common/logger') +const helper = require('../common/helper') +const constants = require('../common/constants') + +const esClient = helper.getESClient() + +/** + * Process create entity message + * @param {Object} message the kafka message + * @param {String} transactionId + */ +async function processCreate (message, transactionId) { + const data = message.payload + const workPeriod = await esClient.getExtra({ + index: config.get('esConfig.ES_INDEX_WORK_PERIOD'), + id: data.workPeriodId + }) + const payments = _.isArray(workPeriod.body.payments) ? workPeriod.body.payments : [] + payments.push(data) + + return esClient.updateExtra({ + index: config.get('esConfig.ES_INDEX_WORK_PERIOD'), + id: data.workPeriodId, + transactionId, + body: { + doc: _.assign(workPeriod.body, { payments }) + }, + refresh: constants.esRefreshOption + }) +} + +processCreate.schema = { + message: Joi.object().keys({ + topic: Joi.string().required(), + originator: Joi.string().required(), + timestamp: Joi.date().required(), + 'mime-type': Joi.string().required(), + payload: Joi.object().keys({ + id: Joi.string().uuid().required(), + workPeriodId: Joi.string().uuid().required(), + challengeId: Joi.string().uuid().required(), + amount: Joi.number().greater(0).allow(null), + status: Joi.workPeriodPaymentStatus().required(), + billingAccountId: Joi.number().allow(null), + createdAt: Joi.date().required(), + createdBy: Joi.string().uuid().required(), + updatedAt: Joi.date().allow(null), + updatedBy: Joi.string().uuid().allow(null) + }).required() + }).required(), + transactionId: Joi.string().required() +} + +/** + * Process update entity message + * @param {Object} message the kafka message + * @param {String} transactionId + */ +async function processUpdate (message, transactionId) { + const data = message.payload + let workPeriod = await esClient.search({ + index: config.get('esConfig.ES_INDEX_WORK_PERIOD'), + body: { + query: { + nested: { + path: 'payments', + query: { + match: { 'payments.id': data.id } + } + } + } + } + }) + if (!workPeriod.body.hits.total.value) { + throw new Error(`id: ${data.id} "WorkPeriodPayments" not found`) + } + let payments + // if WorkPeriodPayment's workPeriodId changed then it must be deleted from the old WorkPeriod + // and added to the new WorkPeriod + if (workPeriod.body.hits.hits[0]._source.id !== data.workPeriodId) { + payments = _.filter(workPeriod.body.hits.hits[0]._source.payments, (payment) => payment.id !== data.id) + await esClient.updateExtra({ + index: config.get('esConfig.ES_INDEX_WORK_PERIOD'), + id: workPeriod.body.hits.hits[0]._source.id, + transactionId, + body: { + doc: _.assign(workPeriod.body.hits.hits[0]._source, { payments }) + } + }) + workPeriod = await esClient.getExtra({ + index: config.get('esConfig.ES_INDEX_WORK_PERIOD'), + id: data.workPeriodId + }) + payments = _.isArray(workPeriod.body.payments) ? workPeriod.body.payments : [] + payments.push(data) + return esClient.updateExtra({ + index: config.get('esConfig.ES_INDEX_WORK_PERIOD'), + id: data.workPeriodId, + transactionId, + body: { + doc: _.assign(workPeriod.body, { payments }) + } + }) + } + + payments = _.map(workPeriod.body.hits.hits[0]._source.payments, (payment) => { + if (payment.id === data.id) { + return _.assign(payment, data) + } + return payment + }) + + return esClient.updateExtra({ + index: config.get('esConfig.ES_INDEX_WORK_PERIOD'), + id: data.workPeriodId, + transactionId, + body: { + doc: _.assign(workPeriod.body.hits.hits[0]._source, { payments }) + }, + refresh: constants.esRefreshOption + }) +} + +processUpdate.schema = processCreate.schema + +module.exports = { + processCreate, + processUpdate +} + +logger.buildService(module.exports, 'WorkPeriodPaymentProcessorService') diff --git a/src/services/WorkPeriodProcessorService.js b/src/services/WorkPeriodProcessorService.js new file mode 100644 index 0000000..568c746 --- /dev/null +++ b/src/services/WorkPeriodProcessorService.js @@ -0,0 +1,109 @@ +/** + * WorkPeriod Processor Service + */ + +const Joi = require('@hapi/joi') +const logger = require('../common/logger') +const helper = require('../common/helper') +const constants = require('../common/constants') +const config = require('config') + +const esClient = helper.getESClient() + +/** + * Process create entity message + * @param {Object} message the kafka message + * @param {String} transactionId + */ +async function processCreate (message, transactionId) { + const workPeriod = message.payload + await esClient.createExtra({ + index: config.get('esConfig.ES_INDEX_WORK_PERIOD'), + id: workPeriod.id, + transactionId, + body: workPeriod, + refresh: constants.esRefreshOption + }) +} + +processCreate.schema = { + message: Joi.object().keys({ + topic: Joi.string().required(), + originator: Joi.string().required(), + timestamp: Joi.date().required(), + 'mime-type': Joi.string().required(), + payload: Joi.object().keys({ + id: Joi.string().uuid().required(), + resourceBookingId: Joi.string().uuid().required(), + userHandle: Joi.string().required(), + projectId: Joi.number().integer().required(), + startDate: Joi.string().required(), + endDate: Joi.string().required(), + daysWorked: Joi.number().integer().min(0).allow(null), + memberRate: Joi.number().allow(null), + customerRate: Joi.number().allow(null), + paymentStatus: Joi.paymentStatus().required(), + createdAt: Joi.date().required(), + createdBy: Joi.string().uuid().required(), + updatedAt: Joi.date().allow(null), + updatedBy: Joi.string().uuid().allow(null) + }).required() + }).required(), + transactionId: Joi.string().required() +} + +/** + * Process update entity message + * @param {Object} message the kafka message + * @param {String} transactionId + */ +async function processUpdate (message, transactionId) { + const data = message.payload + await esClient.updateExtra({ + index: config.get('esConfig.ES_INDEX_WORK_PERIOD'), + id: data.id, + transactionId, + body: { + doc: data + }, + refresh: constants.esRefreshOption + }) +} + +processUpdate.schema = processCreate.schema + +/** + * Process delete entity message + * @param {Object} message the kafka message + * @param {String} transactionId + */ +async function processDelete (message, transactionId) { + const id = message.payload.id + await esClient.deleteExtra({ + index: config.get('esConfig.ES_INDEX_WORK_PERIOD'), + id, + transactionId, + refresh: constants.esRefreshOption + }) +} + +processDelete.schema = { + message: Joi.object().keys({ + topic: Joi.string().required(), + originator: Joi.string().required(), + timestamp: Joi.date().required(), + 'mime-type': Joi.string().required(), + payload: Joi.object().keys({ + id: Joi.string().uuid().required() + }).required() + }).required(), + transactionId: Joi.string().required() +} + +module.exports = { + processCreate, + processUpdate, + processDelete +} + +logger.buildService(module.exports, 'WorkPeriodProcessorService') diff --git a/test/e2e/test.js b/test/e2e/test.js index 0c32a5c..1e09ccc 100644 --- a/test/e2e/test.js +++ b/test/e2e/test.js @@ -3,7 +3,6 @@ */ const config = require('config') -const _ = require('lodash') const stringcase = require('stringcase') const app = require('../../src/app') const request = require('superagent') @@ -89,14 +88,14 @@ describe('Taas ES Processor E2E Test', () => { index, id: testData.messages[model].create.message.payload.id }) - should.deepEqual(doc.body._source, _.omit(testData.messages[model].create.message.payload, ['id'])) + should.deepEqual(doc.body._source, testData.messages[model].create.message.payload, ['id']) }) it(`Should handle ${modelInSpaceCase} updating message`, async () => { await testHelper.esClient.create({ index, id: testData.messages[model].create.message.payload.id, - body: _.omit(testData.messages[model].create.message.payload, ['id']), + body: testData.messages[model].create.message.payload, refresh: 'true' }) await testHelper.sendMessage(testData.messages[model].update.message) @@ -105,14 +104,14 @@ describe('Taas ES Processor E2E Test', () => { index, id: testData.messages[model].update.message.payload.id }) - should.deepEqual(_.omit(doc.body._source, ['createdAt', 'createdBy']), _.omit(testData.messages[model].update.message.payload, ['id'])) + should.deepEqual(doc.body._source, testData.messages[model].update.message.payload) }) it(`Should handle ${modelInSpaceCase} deletion message`, async () => { await testHelper.esClient.create({ index, id: testData.messages[model].create.message.payload.id, - body: _.omit(testData.messages[model].create.message.payload, ['id']), + body: testData.messages[model].create.message.payload, refresh: 'true' }) await testHelper.sendMessage(testData.messages[model].delete.message) @@ -133,7 +132,7 @@ describe('Taas ES Processor E2E Test', () => { await testHelper.esClient.create({ index, id: testData.messages[model].create.message.payload.id, - body: _.omit(testData.messages[model].create.message.payload, ['id']), + body: testData.messages[model].create.message.payload, refresh: 'true' }) await testHelper.sendMessage(testData.messages[model].create.message) diff --git a/test/messages/taas.job.create.event.json b/test/messages/taas.job.create.event.json index b6cc0a5..e2b12bb 100644 --- a/test/messages/taas.job.create.event.json +++ b/test/messages/taas.job.create.event.json @@ -1 +1 @@ -{"topic":"taas.job.create","originator":"taas-api","timestamp":"2020-11-05T19:00:17.563Z","mime-type":"application/json","payload":{"projectId":21,"externalId":"1212","description":"Dummy Description","startDate":"2020-09-27T04:17:23.131Z","endDate":"2020-09-27T04:17:23.131Z","numPositions":13,"resourceType":"Dummy Resource Type","rateType":"hourly","skills":["56fdc405-eccc-4189-9e83-c78abf844f50","f91ae184-aba2-4485-a8cb-9336988c05ab","edfc7b4f-636f-44bd-96fc-949ffc58e38b","4ca63bb6-f515-4ab0-a6bc-c2d8531e084f","ee03c041-d53b-4c08-b7d9-80d7461da3e4"],"id":"ffbc24f7-301e-48d3-bf01-c056916056a2","createdAt":"2020-11-05T19:00:16.268Z","createdBy":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"sourcing"}} \ No newline at end of file +{"topic":"taas.job.create","originator":"taas-api","timestamp":"2020-11-05T19:00:17.563Z","mime-type":"application/json","payload":{"title":"Job Title","projectId":21,"externalId":"1212","description":"Dummy Description","startDate":"2020-09-27T04:17:23.131Z","duration":17,"numPositions":13,"resourceType":"Dummy Resource Type","rateType":"hourly","skills":["56fdc405-eccc-4189-9e83-c78abf844f50","f91ae184-aba2-4485-a8cb-9336988c05ab","edfc7b4f-636f-44bd-96fc-949ffc58e38b","4ca63bb6-f515-4ab0-a6bc-c2d8531e084f","ee03c041-d53b-4c08-b7d9-80d7461da3e4"],"id":"ffbc24f7-301e-48d3-bf01-c056916056a2","createdAt":"2020-11-05T19:00:16.268Z","createdBy":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"sourcing","isApplicationPageActive":false}} \ No newline at end of file diff --git a/test/messages/taas.job.update.event.json b/test/messages/taas.job.update.event.json index f5bf395..fced890 100644 --- a/test/messages/taas.job.update.event.json +++ b/test/messages/taas.job.update.event.json @@ -1 +1 @@ -{"topic":"taas.job.update","originator":"taas-api","timestamp":"2020-11-05T19:00:19.015Z","mime-type":"application/json","payload":{"id":"ffbc24f7-301e-48d3-bf01-c056916056a2","projectId":21,"externalId":"1212","description":"Dummy Description","startDate":"2020-09-27T04:17:23.131Z","endDate":"2020-09-27T04:17:23.131Z","numPositions":13,"resourceType":"Dummy Resource Type","rateType":"hourly","skills":["3fa85f64-5717-4562-b3fc-2c963f66afa6","cc41ddc4-cacc-4570-9bdb-1229c12b9784"],"status":"sourcing","updatedAt":"2020-11-05T19:00:17.612Z","updatedBy":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a"}} \ No newline at end of file +{"topic":"taas.job.update","originator":"taas-api","timestamp":"2020-11-05T19:00:19.015Z","mime-type":"application/json","payload":{"id":"ffbc24f7-301e-48d3-bf01-c056916056a2","title":"Job Title Updated","projectId":21,"externalId":"1212","description":"Dummy Description","startDate":"2020-09-27T04:17:23.131Z","duration":19,"numPositions":13,"resourceType":"Dummy Resource Type","rateType":"hourly","skills":["3fa85f64-5717-4562-b3fc-2c963f66afa6","cc41ddc4-cacc-4570-9bdb-1229c12b9784"],"status":"sourcing","updatedAt":"2020-11-05T19:00:17.612Z","updatedBy":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","createdAt":"2020-11-05T19:00:16.268Z","createdBy":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","isApplicationPageActive":false}} \ No newline at end of file diff --git a/test/messages/taas.jobcandidate.update.event.json b/test/messages/taas.jobcandidate.update.event.json index 32d4b7f..79530f9 100644 --- a/test/messages/taas.jobcandidate.update.event.json +++ b/test/messages/taas.jobcandidate.update.event.json @@ -1 +1 @@ -{"topic":"taas.jobcandidate.update","originator":"taas-api","timestamp":"2020-11-05T19:00:23.003Z","mime-type":"application/json","payload":{"id":"0cb99adb-8bcd-4952-9203-9867dd45ef6f","jobId":"ffbc24f7-301e-48d3-bf01-c056916056a2","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"selected","updatedAt":"2020-11-05T19:00:21.625Z","updatedBy":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a"}} \ No newline at end of file +{"topic":"taas.jobcandidate.update","originator":"taas-api","timestamp":"2020-11-05T19:00:23.003Z","mime-type":"application/json","payload":{"id":"0cb99adb-8bcd-4952-9203-9867dd45ef6f","jobId":"ffbc24f7-301e-48d3-bf01-c056916056a2","userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"selected","updatedAt":"2020-11-05T19:00:21.625Z","updatedBy":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","createdAt":"2020-11-05T19:00:16.268Z","createdBy":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a"}} \ No newline at end of file diff --git a/test/messages/taas.resourcebooking.create.event.json b/test/messages/taas.resourcebooking.create.event.json index 1a358ba..2f3de0a 100644 --- a/test/messages/taas.resourcebooking.create.event.json +++ b/test/messages/taas.resourcebooking.create.event.json @@ -1 +1,21 @@ -{"topic":"taas.resourcebooking.create","originator":"taas-api","timestamp":"2020-11-05T19:00:25.038Z","mime-type":"application/json","payload":{"projectId":21,"userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","jobId":"ffbc24f7-301e-48d3-bf01-c056916056a2","startDate":"2020-09-27T04:17:23.131Z","endDate":"2020-09-27T04:17:23.131Z","memberRate":13.23,"customerRate":13,"rateType":"hourly","id":"60d97713-8621-476e-b006-7cb9589c7777","createdAt":"2020-11-05T19:00:23.036Z","createdBy":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","status":"sourcing"}} \ No newline at end of file +{ + "topic": "taas.resourcebooking.create", + "originator": "taas-api", + "timestamp": "2020-11-05T19:00:25.038Z", + "mime-type": "application/json", + "payload": { + "projectId": 21, + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "jobId": "ffbc24f7-301e-48d3-bf01-c056916056a2", + "startDate": "2020-09-27", + "endDate": "2020-09-27", + "memberRate": 13.23, + "customerRate": 13, + "rateType": "hourly", + "id": "60d97713-8621-476e-b006-7cb9589c7777", + "createdAt": "2020-11-05T19:00:23.036Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "status": "assigned", + "billingAccountId": 80000071 + } +} diff --git a/test/messages/taas.resourcebooking.update.event.json b/test/messages/taas.resourcebooking.update.event.json index de8080e..2b96229 100644 --- a/test/messages/taas.resourcebooking.update.event.json +++ b/test/messages/taas.resourcebooking.update.event.json @@ -1 +1,23 @@ -{"topic":"taas.resourcebooking.update","originator":"taas-api","timestamp":"2020-11-05T19:00:26.407Z","mime-type":"application/json","payload":{"id":"60d97713-8621-476e-b006-7cb9589c7777","projectId":21,"userId":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a","jobId":"ffbc24f7-301e-48d3-bf01-c056916056a2","startDate":"2020-09-27T04:17:23.131Z","endDate":"2020-09-27T04:17:23.131Z","memberRate":13.23,"customerRate":13,"rateType":"hourly","status":"assigned","updatedAt":"2020-11-05T19:00:25.062Z","updatedBy":"a55fe1bc-1754-45fa-9adc-cf3d6d7c377a"}} \ No newline at end of file +{ + "topic": "taas.resourcebooking.update", + "originator": "taas-api", + "timestamp": "2020-11-05T19:00:26.407Z", + "mime-type": "application/json", + "payload": { + "id": "60d97713-8621-476e-b006-7cb9589c7777", + "projectId": 21, + "userId": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "jobId": "ffbc24f7-301e-48d3-bf01-c056916056a2", + "startDate": "2020-09-27", + "endDate": "2020-09-27", + "memberRate": 13.23, + "customerRate": 13, + "rateType": "hourly", + "status": "assigned", + "billingAccountId": 80000071, + "updatedAt": "2020-11-05T19:00:25.062Z", + "updatedBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a", + "createdAt": "2020-11-05T19:00:16.268Z", + "createdBy": "a55fe1bc-1754-45fa-9adc-cf3d6d7c377a" + } +} diff --git a/test/messages/taas.workperiod.create.event.json b/test/messages/taas.workperiod.create.event.json new file mode 100644 index 0000000..3c2d286 --- /dev/null +++ b/test/messages/taas.workperiod.create.event.json @@ -0,0 +1,23 @@ +{ + "topic": "taas.workperiod.create", + "originator": "taas-api", + "timestamp": "2021-03-30T20:24:17.555Z", + "mime-type": "application/json", + "payload": { + "resourceBookingId": "6cf2edf6-4b2c-40ef-96db-e1ddb771fdd3", + "startDate": "2021-03-14", + "endDate": "2021-03-20", + "daysWorked": 3, + "memberRate": 13.13, + "customerRate": 13.13, + "paymentStatus": "cancelled", + "projectId": 111, + "userHandle": "pshah_manager", + "id": "926040c4-1709-4de2-b2b6-52adf6e5e72d", + "billingAccountId": 80000071 + "createdBy": "00000000-0000-0000-0000-000000000000", + "updatedAt": "2021-03-30T20:24:17.541Z", + "createdAt": "2021-03-30T20:24:17.541Z", + "updatedBy": null + } +} diff --git a/test/messages/taas.workperiod.delete.event.json b/test/messages/taas.workperiod.delete.event.json new file mode 100644 index 0000000..3b3207c --- /dev/null +++ b/test/messages/taas.workperiod.delete.event.json @@ -0,0 +1,9 @@ +{ + "topic": "taas.workperiod.delete", + "originator": "taas-api", + "timestamp": "2021-03-30T20:13:58.491Z", + "mime-type": "application/json", + "payload": { + "id": "926040c4-1709-4de2-b2b6-52adf6e5e72d" + } +} \ No newline at end of file diff --git a/test/messages/taas.workperiod.update.event.json b/test/messages/taas.workperiod.update.event.json new file mode 100644 index 0000000..0e8ca71 --- /dev/null +++ b/test/messages/taas.workperiod.update.event.json @@ -0,0 +1,21 @@ +{ + "topic": "taas.workperiod.update", + "originator": "taas-api", + "timestamp": "2021-03-30T20:13:53.179Z", + "mime-type": "application/json", + "payload": { + "id": "926040c4-1709-4de2-b2b6-52adf6e5e72d", + "resourceBookingId": "79317ff6-5b30-45c2-ace8-b97282b042a8", + "startDate": "2021-03-14", + "endDate": "2021-03-20", + "daysWorked": 3, + "memberRate": 13.13, + "customerRate": 13.13, + "paymentStatus": "pending", + "projectId": 111, + "userHandle": "pshah_manager", + "createdBy": "00000000-0000-0000-0000-000000000000", + "createdAt": "2021-03-30T20:13:34.670Z", + "updatedAt": "2021-03-30T20:13:45.354Z" + } +} \ No newline at end of file diff --git a/test/messages/taas.workperiodpayment.create.event.json b/test/messages/taas.workperiodpayment.create.event.json new file mode 100644 index 0000000..25ab9cc --- /dev/null +++ b/test/messages/taas.workperiodpayment.create.event.json @@ -0,0 +1 @@ +{"topic":"taas.workperiodpayment.create","originator":"taas-api","timestamp":"2021-04-09T20:10:33.770Z","mime-type":"application/json","payload":{"challengeId":"00000000-0000-0000-0000-000000000000","workPeriodId":"140b7407-540d-40c3-ad23-905d932aa9c8","amount":600,"status":"completed","id":"09c80ee6-21be-45a4-9c3c-7ec4c75ece79","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedAt":"2021-04-09T20:10:33.755Z","createdAt":"2021-04-09T20:10:33.755Z","updatedBy":null}} \ No newline at end of file diff --git a/test/messages/taas.workperiodpayment.update.event.json b/test/messages/taas.workperiodpayment.update.event.json new file mode 100644 index 0000000..66e5bce --- /dev/null +++ b/test/messages/taas.workperiodpayment.update.event.json @@ -0,0 +1 @@ +{"topic":"taas.workperiodpayment.update","originator":"taas-api","timestamp":"2021-04-09T20:12:26.994Z","mime-type":"application/json","payload":{"id":"09c80ee6-21be-45a4-9c3c-7ec4c75ece79","workPeriodId":"140b7407-540d-40c3-ad23-905d932aa9c8","challengeId":"00000000-0000-0000-0000-000000000000","amount":1600,"status":"completed","createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","updatedBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c","createdAt":"2021-04-09T20:10:33.755Z","updatedAt":"2021-04-09T20:12:26.966Z"}} \ No newline at end of file diff --git a/test/unit/test.js b/test/unit/test.js index 05426bd..6f9ca67 100644 --- a/test/unit/test.js +++ b/test/unit/test.js @@ -8,24 +8,45 @@ const config = require('config') const stringcase = require('stringcase') const testData = require('../common/testData') const testHelper = require('../common/testHelper') +const sinon = require('sinon') +const logger = require('../../src/common/logger') +const helper = require('../../src/common/helper') +const constants = require('../../src/common/constants') const services = { JobProcessorService: require('../../src/services/JobProcessorService'), JobCandidateProcessorService: require('../../src/services/JobCandidateProcessorService'), ResourceBookingProcessorService: require('../../src/services/ResourceBookingProcessorService') } -describe('UBahn - Elasticsearch Data Processor Unit Test', () => { - // random transaction id here - const transactionId = '2023692c-a9d3-4250-86c4-b83f381a5d03' +// random transaction id here +const transactionId = '2023692c-a9d3-4250-86c4-b83f381a5d03' - after(() => { +describe('General Logic Tests', () => { + let sandbox + + before(() => { + // mock helper methods + sandbox = sinon.createSandbox() + sandbox.stub(helper, 'postMessageViaWebhook').callsFake((webhook, message) => { + logger.debug({ component: 'helper', context: 'postMessageToZapier (stub)', message: `message: ${JSON.stringify({ webhook, message })}` }) + }) + sandbox.stub(helper, 'getM2MToken').callsFake(() => { + const token = 'dummy-token' + logger.debug({ component: 'helper', context: 'getM2MToken (stub)', message: token }) + return token + }) + }) + + beforeEach(() => { // clear es storage testData.esStorage.content = {} }) - beforeEach(() => { + after(() => { // clear es storage testData.esStorage.content = {} + + sandbox.restore() }) for (const [index, model] of [ @@ -38,7 +59,7 @@ describe('UBahn - Elasticsearch Data Processor Unit Test', () => { await services[`${model}ProcessorService`].processCreate(testData.messages[model].create.message, transactionId) should.deepEqual( testData.esStorage.content[testData.messages[model].create.message.payload.id], - _.omit(testData.messages[model].create.message.payload, ['id']) + testData.messages[model].create.message.payload ) }) @@ -46,13 +67,13 @@ describe('UBahn - Elasticsearch Data Processor Unit Test', () => { await testHelper.esClient.create({ index, id: testData.messages[model].create.message.payload.id, - body: _.omit(testData.messages[model].create.message.payload, ['id']), + body: testData.messages[model].create.message.payload, refresh: 'true' }) await services[`${model}ProcessorService`].processUpdate(testData.messages[model].update.message, transactionId) should.deepEqual( - _.omit(testData.esStorage.content[testData.messages[model].create.message.payload.id], ['createdAt', 'createdBy']), - _.omit(testData.messages[model].update.message.payload, ['id']) + testData.esStorage.content[testData.messages[model].create.message.payload.id], + testData.messages[model].update.message.payload ) }) @@ -60,7 +81,7 @@ describe('UBahn - Elasticsearch Data Processor Unit Test', () => { await testHelper.esClient.create({ index, id: testData.messages[model].create.message.payload.id, - body: _.omit(testData.messages[model].create.message.payload, ['id']), + body: testData.messages[model].create.message.payload, refresh: 'true' }) await services[`${model}ProcessorService`].processDelete(testData.messages[model].delete.message, transactionId) @@ -71,7 +92,7 @@ describe('UBahn - Elasticsearch Data Processor Unit Test', () => { await testHelper.esClient.create({ index, id: testData.messages[model].create.message.payload.id, - body: _.omit(testData.messages[model].create.message.payload, ['id']), + body: testData.messages[model].create.message.payload, refresh: 'true' }) try { @@ -101,3 +122,178 @@ describe('UBahn - Elasticsearch Data Processor Unit Test', () => { }) } }) + +describe('Zapier Logic Tests', () => { + let sandbox + + beforeEach(() => { + // clear es storage + testData.esStorage.content = {} + + // mock helper methods + sandbox = sinon.createSandbox() + sandbox.stub(helper, 'postMessageViaWebhook').callsFake((webhook, message) => { + logger.debug({ component: 'helper', context: 'postMessageToZapier (stub)', message: `message: ${JSON.stringify({ webhook, message })}` }) + }) + sandbox.stub(helper, 'getM2MToken').callsFake(() => { + const token = 'dummy-token' + logger.debug({ component: 'helper', context: 'getM2MToken (stub)', message: token }) + return token + }) + }) + + afterEach(() => { + // clear es storage + testData.esStorage.content = {} + + // reset mocked methods + sandbox.restore() + }) + + it('should have Zapier switched ON during testing Zapier logic', () => { + // to enable Job Candidates Zapier logic + should.equal(config.zapier.ZAPIER_JOB_CANDIDATE_SWITCH, constants.Zapier.Switch.ON) + // to enable Jobs Zapier logic + should.equal(config.zapier.ZAPIER_SWITCH, constants.Zapier.Switch.ON) + }) + + describe('Job Candidate Update', () => { + it('should post to Zapier if status is changed to "rejected"', async () => { + const previousData = _.assign({}, testData.messages.JobCandidate.create.message.payload, { status: 'open', externalId: '123' }) + const updateMessage = _.assign({}, testData.messages.JobCandidate.update.message, { + payload: _.assign({}, testData.messages.JobCandidate.update.message.payload, { status: 'rejected', externalId: '123' }) + }) + + await testHelper.esClient.create({ + index: config.esConfig.ES_INDEX_JOB, + id: previousData.id, + body: previousData, + refresh: 'true' + }) + await testHelper.esClient.create({ + index: config.esConfig.ES_INDEX_JOB_CANDIDATE, + id: testData.messages.Job.create.message.payload.id, + body: testData.messages.Job.create.message.payload, + refresh: 'true' + }) + await services[`JobCandidateProcessorService`].processUpdate(updateMessage, transactionId) + + helper.postMessageViaWebhook.callCount.should.equal(1) + }) + + it('should post to Zapier if status is changed to "shortlist"', async () => { + const previousData = _.assign({}, testData.messages.JobCandidate.create.message.payload, { status: 'open', externalId: '123' }) + const updateMessage = _.assign({}, testData.messages.JobCandidate.update.message, { + payload: _.assign({}, testData.messages.JobCandidate.update.message.payload, { status: 'shortlist', externalId: '123' }) + }) + + await testHelper.esClient.create({ + index: config.esConfig.ES_INDEX_JOB, + id: previousData.id, + body: previousData, + refresh: 'true' + }) + await testHelper.esClient.create({ + index: config.esConfig.ES_INDEX_JOB_CANDIDATE, + id: testData.messages.Job.create.message.payload.id, + body: testData.messages.Job.create.message.payload, + refresh: 'true' + }) + await services[`JobCandidateProcessorService`].processUpdate(updateMessage, transactionId) + + helper.postMessageViaWebhook.callCount.should.equal(1) + }) + + it('should not post to Zapier if status was already "rejected"', async () => { + const previousData = _.assign({}, testData.messages.JobCandidate.create.message.payload, { status: 'rejected', externalId: '123' }) + const updateMessage = _.assign({}, testData.messages.JobCandidate.update.message, { + payload: _.assign({}, testData.messages.JobCandidate.update.message.payload, { status: 'rejected', externalId: '123' }) + }) + + await testHelper.esClient.create({ + index: config.esConfig.ES_INDEX_JOB, + id: previousData.id, + body: previousData, + refresh: 'true' + }) + await testHelper.esClient.create({ + index: config.esConfig.ES_INDEX_JOB_CANDIDATE, + id: testData.messages.Job.create.message.payload.id, + body: testData.messages.Job.create.message.payload, + refresh: 'true' + }) + await services[`JobCandidateProcessorService`].processUpdate(updateMessage, transactionId) + + helper.postMessageViaWebhook.callCount.should.equal(0) + }) + + it('should not post to Zapier if status was already "shortlist"', async () => { + const previousData = _.assign({}, testData.messages.JobCandidate.create.message.payload, { status: 'shortlist', externalId: '123' }) + const updateMessage = _.assign({}, testData.messages.JobCandidate.update.message, { + payload: _.assign({}, testData.messages.JobCandidate.update.message.payload, { status: 'shortlist', externalId: '123' }) + }) + + await testHelper.esClient.create({ + index: config.esConfig.ES_INDEX_JOB, + id: previousData.id, + body: previousData, + refresh: 'true' + }) + await testHelper.esClient.create({ + index: config.esConfig.ES_INDEX_JOB_CANDIDATE, + id: testData.messages.Job.create.message.payload.id, + body: testData.messages.Job.create.message.payload, + refresh: 'true' + }) + await services[`JobCandidateProcessorService`].processUpdate(updateMessage, transactionId) + + helper.postMessageViaWebhook.callCount.should.equal(0) + }) + + it('should not post to Zapier if status is changed to "interview" (not "rejected" or "shortlist")', async () => { + const previousData = _.assign({}, testData.messages.JobCandidate.create.message.payload, { status: 'open', externalId: '123' }) + const updateMessage = _.assign({}, testData.messages.JobCandidate.update.message, { + payload: _.assign({}, testData.messages.JobCandidate.update.message.payload, { status: 'interview', externalId: '123' }) + }) + + await testHelper.esClient.create({ + index: config.esConfig.ES_INDEX_JOB, + id: previousData.id, + body: previousData, + refresh: 'true' + }) + await testHelper.esClient.create({ + index: config.esConfig.ES_INDEX_JOB_CANDIDATE, + id: testData.messages.Job.create.message.payload.id, + body: testData.messages.Job.create.message.payload, + refresh: 'true' + }) + await services[`JobCandidateProcessorService`].processUpdate(updateMessage, transactionId) + + helper.postMessageViaWebhook.callCount.should.equal(0) + }) + + it('should not post to Zapier if status is changed to "topcoder-rejected" (not "rejected" or "shortlist")', async () => { + const previousData = _.assign({}, testData.messages.JobCandidate.create.message.payload, { status: 'open', externalId: '123' }) + const updateMessage = _.assign({}, testData.messages.JobCandidate.update.message, { + payload: _.assign({}, testData.messages.JobCandidate.update.message.payload, { status: 'topcoder-rejected', externalId: '123' }) + }) + + await testHelper.esClient.create({ + index: config.esConfig.ES_INDEX_JOB, + id: previousData.id, + body: previousData, + refresh: 'true' + }) + await testHelper.esClient.create({ + index: config.esConfig.ES_INDEX_JOB_CANDIDATE, + id: testData.messages.Job.create.message.payload.id, + body: testData.messages.Job.create.message.payload, + refresh: 'true' + }) + await services[`JobCandidateProcessorService`].processUpdate(updateMessage, transactionId) + + helper.postMessageViaWebhook.callCount.should.equal(0) + }) + }) +})