Skip to content
Merged
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ To be able to change and test `taas-es-processor` locally you can follow the nex
| `npm run cov` | Code Coverage Report. |
| `npm run migrate` | Run any migration files which haven't run yet. |
| `npm run migrate:undo` | Revert most recent migration. |
| `npm run demo-payment-scheduler` | Create 1000 Work Periods Payment records in with status "scheduled" and various "amount" |

## Import and Export data

Expand Down
24 changes: 21 additions & 3 deletions app-constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,24 @@ const ChallengeStatus = {

const WorkPeriodPaymentStatus = {
COMPLETED: 'completed',
CANCELLED: 'cancelled',
SCHEDULED: 'scheduled'
SCHEDULED: 'scheduled',
IN_PROGRESS: 'in-progress',
FAILED: 'failed',
CANCELLED: 'cancelled'
}

const PaymentProcessingSwitch = {
ON: 'ON',
OFF: 'OFF'
}

const PaymentSchedulerStatus = {
START_PROCESS: 'start-process',
CREATE_CHALLENGE: 'create-challenge',
ASSIGN_MEMBER: 'assign-member',
ACTIVATE_CHALLENGE: 'activate-challenge',
GET_USER_ID: 'get-userId',
CLOSE_CHALLENGE: 'close-challenge'
}

module.exports = {
Expand All @@ -96,5 +112,7 @@ module.exports = {
Scopes,
Interviews,
ChallengeStatus,
WorkPeriodPaymentStatus
WorkPeriodPaymentStatus,
PaymentSchedulerStatus,
PaymentProcessingSwitch
}
4 changes: 4 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const schedule = require('node-schedule')
const logger = require('./src/common/logger')
const eventHandlers = require('./src/eventHandlers')
const interviewService = require('./src/services/InterviewService')
const { processScheduler } = require('./src/services/PaymentSchedulerService')

// setup express app
const app = express()
Expand Down Expand Up @@ -97,6 +98,9 @@ const server = app.listen(app.get('port'), () => {
eventHandlers.init()
// schedule updateCompletedInterviews to run every hour
schedule.scheduleJob('0 0 * * * *', interviewService.updateCompletedInterviews)

// schedule payment processing
schedule.scheduleJob(config.PAYMENT_PROCESSING.CRON, processScheduler)
})

if (process.env.NODE_ENV === 'test') {
Expand Down
37 changes: 36 additions & 1 deletion config/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,5 +175,40 @@ module.exports = {
// the minimum matching rate when searching roles by skills
ROLE_MATCHING_RATE: process.env.ROLE_MATCHING_RATE || 0.70,
// member groups representing Wipro or TopCoder employee
INTERNAL_MEMBER_GROUPS: process.env.INTERNAL_MEMBER_GROUPS || ['20000000', '20000001', '20000003', '20000010', '20000015']
INTERNAL_MEMBER_GROUPS: process.env.INTERNAL_MEMBER_GROUPS || ['20000000', '20000001', '20000003', '20000010', '20000015'],
// payment scheduler config
PAYMENT_PROCESSING: {
// switch off actual API calls in Payment Scheduler
SWITCH: process.env.PAYMENT_PROCESSING_SWITCH || 'OFF',
// the payment scheduler cron config
CRON: process.env.PAYMENT_PROCESSING_CRON || '0 */5 * * * *',
// the number of records processed by one time
BATCH_SIZE: parseInt(process.env.PAYMENT_PROCESSING_BATCH_SIZE || 50),
// in-progress expired to determine whether a record has been processed abnormally, moment duration format
IN_PROGRESS_EXPIRED: process.env.IN_PROGRESS_EXPIRED || 'PT1H',
// the number of max retry config
MAX_RETRY_COUNT: parseInt(process.env.PAYMENT_PROCESSING_MAX_RETRY_COUNT || 10),
// the time of retry base delay, unit: ms
RETRY_BASE_DELAY: parseInt(process.env.PAYMENT_PROCESSING_RETRY_BASE_DELAY || 100),
// the time of retry max delay, unit: ms
RETRY_MAX_DELAY: parseInt(process.env.PAYMENT_PROCESSING_RETRY_MAX_DELAY || 10000),
// the max time of one request, unit: ms
PER_REQUEST_MAX_TIME: parseInt(process.env.PAYMENT_PROCESSING_PER_REQUEST_MAX_TIME || 30000),
// the max time of one payment record, unit: ms
PER_PAYMENT_MAX_TIME: parseInt(process.env.PAYMENT_PROCESSING_PER_PAYMENT_MAX_TIME || 60000),
// the max records of payment of a minute
PER_MINUTE_PAYMENT_MAX_COUNT: parseInt(process.env.PAYMENT_PROCESSING_PER_MINUTE_PAYMENT_MAX_COUNT || 12),
// the max requests of challenge of a minute
PER_MINUTE_CHALLENGE_REQUEST_MAX_COUNT: parseInt(process.env.PAYMENT_PROCESSING_PER_MINUTE_CHALLENGE_REQUEST_MAX_COUNT || 60),
// the max requests of resource of a minute
PER_MINUTE_RESOURCE_REQUEST_MAX_COUNT: parseInt(process.env.PAYMENT_PROCESSING_PER_MINUTE_CHALLENGE_REQUEST_MAX_COUNT || 20),
// the default step fix delay, unit: ms
FIX_DELAY_STEP: parseInt(process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP || 500),
// the fix delay after step of create challenge, unit: ms
FIX_DELAY_STEP_CREATE_CHALLENGE: parseInt(process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP_CREATE_CHALLENGE || process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP || 500),
// the fix delay after step of assign member, unit: ms
FIX_DELAY_STEP_ASSIGN_MEMBER: parseInt(process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP_ASSIGN_MEMBER || process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP || 500),
// the fix delay after step of activate challenge, unit: ms
FIX_DELAY_STEP_ACTIVATE_CHALLENGE: parseInt(process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP_ACTIVATE_CHALLENGE || process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP || 500)
}
}
24 changes: 19 additions & 5 deletions docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2403,7 +2403,7 @@ paths:
required: false
schema:
type: string
enum: ["completed", "scheduled", "cancelled"]
enum: ["completed", "scheduled", "in-progress", "failed", "cancelled"]
description: The payment status.
responses:
"200":
Expand Down Expand Up @@ -2519,7 +2519,7 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Error"

/work-period-payments/{id}:
get:
tags:
Expand Down Expand Up @@ -4830,8 +4830,22 @@ components:
description: "The amount to be paid."
status:
type: string
enum: ["completed", "scheduled", "cancelled"]
enum: ["completed", "scheduled", "in-progress", "failed", "cancelled"]
description: "The payment status."
statusDetails:
type: object
properties:
errorMessage:
type: string
errorCode:
type: integer
retry:
type: integer
step:
type: string
challengeId:
type: string
format: uuid
billingAccountId:
type: integer
example: 80000071
Expand Down Expand Up @@ -4888,7 +4902,7 @@ components:
description: "The amount to be paid."
status:
type: string
enum: ["completed", "scheduled", "cancelled"]
enum: ["completed", "scheduled", "in-progress", "failed", "cancelled"]
description: "The payment status."
WorkPeriodPaymentCreateRequestBody:
required:
Expand Down Expand Up @@ -4979,7 +4993,7 @@ components:
description: "The amount to be paid."
status:
type: string
enum: ["completed", "scheduled", "cancelled"]
enum: ["completed", "scheduled", "in-progress", "failed", "cancelled"]
description: "The payment status."
CheckRun:
type: object
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
'use strict';

const config = require('config')
const _ = require('lodash')
const { PaymentSchedulerStatus } = require('../app-constants')

/**
* Create `payment_schedulers` table & relations.
*/
module.exports = {
up: async (queryInterface, Sequelize) => {
const transaction = await queryInterface.sequelize.transaction()
try {
await queryInterface.createTable('payment_schedulers', {
id: {
type: Sequelize.UUID,
primaryKey: true,
allowNull: false,
defaultValue: Sequelize.UUIDV4
},
challengeId: {
field: 'challenge_id',
type: Sequelize.UUID,
allowNull: false
},
workPeriodPaymentId: {
field: 'work_period_payment_id',
type: Sequelize.UUID,
allowNull: false,
references: {
model: {
tableName: 'work_period_payments',
schema: config.DB_SCHEMA_NAME
},
key: 'id'
}
},
step: {
type: Sequelize.ENUM(_.values(PaymentSchedulerStatus)),
allowNull: false
},
status: {
type: Sequelize.ENUM(
'in-progress',
'completed',
'failed'
),
allowNull: false
},
userId: {
field: 'user_id',
type: Sequelize.BIGINT
},
userHandle: {
field: 'user_handle',
type: Sequelize.STRING,
allowNull: false
},
createdAt: {
field: 'created_at',
type: Sequelize.DATE
},
updatedAt: {
field: 'updated_at',
type: Sequelize.DATE
},
deletedAt: {
field: 'deleted_at',
type: Sequelize.DATE
}
}, { schema: config.DB_SCHEMA_NAME, transaction })
await queryInterface.addColumn({ tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME }, 'status_details',
{ type: Sequelize.JSONB },
{ transaction })
await queryInterface.changeColumn({ tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME }, 'challenge_id',
{ type: Sequelize.UUID },
{ transaction })
await queryInterface.sequelize.query(`ALTER TYPE ${config.DB_SCHEMA_NAME}.enum_work_period_payments_status ADD VALUE 'scheduled'`)
await queryInterface.sequelize.query(`ALTER TYPE ${config.DB_SCHEMA_NAME}.enum_work_period_payments_status ADD VALUE 'in-progress'`)
await queryInterface.sequelize.query(`ALTER TYPE ${config.DB_SCHEMA_NAME}.enum_work_period_payments_status ADD VALUE 'failed'`)
await transaction.commit()
} catch (err) {
await transaction.rollback()
throw err
}
},

down: async (queryInterface, Sequelize) => {
const table = { schema: config.DB_SCHEMA_NAME, tableName: 'payment_schedulers' }
const statusTypeName = `${table.schema}.enum_${table.tableName}_status`
const stepTypeName = `${table.schema}.enum_${table.tableName}_step`
const transaction = await queryInterface.sequelize.transaction()
try {
await queryInterface.dropTable(table, { transaction })
// drop enum type for status and step column
await queryInterface.sequelize.query(`DROP TYPE ${statusTypeName}`, { transaction })
await queryInterface.sequelize.query(`DROP TYPE ${stepTypeName}`, { transaction })

await queryInterface.changeColumn({ tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME }, 'challenge_id',
{ type: Sequelize.UUID, allowNull: false },
{ transaction })
await queryInterface.removeColumn({ tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME }, 'status_details',
{ transaction })
await queryInterface.sequelize.query(`DELETE FROM pg_enum WHERE enumlabel in ('scheduled', 'in-progress', 'failed') AND enumtypid = (SELECT oid FROM pg_type WHERE typname = 'enum_work_period_payments_status')`,
{ transaction })
await transaction.commit()
} catch (err) {
await transaction.rollback()
throw err
}
}
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"local:init": "npm run local:reset && npm run data:import -- --force",
"local:reset": "npm run delete-index -- --force || true && npm run create-index -- --force && npm run init-db force",
"cov": "nyc --reporter=html --reporter=text npm run test",
"demo-payment-scheduler": "node scripts/demo-payment-scheduler/index.js && npm run index:all -- --force",
"demo-payment": "node scripts/demo-payment"
},
"keywords": [],
Expand Down
103 changes: 103 additions & 0 deletions scripts/demo-payment-scheduler/data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
{
"Job":{
"id":"43d695d4-e926-41d5-ad42-a899612b5246",
"projectId":17234,
"title":"Dummy title - at most 64 characters",
"numPositions":13,
"skills":[
"23e00d92-207a-4b5b-b3c9-4c5662644941",
"7d076384-ccf6-4e43-a45d-1b24b1e624aa",
"cbac57a3-7180-4316-8769-73af64893158",
"a2b4bc11-c641-4a19-9eb7-33980378f82e"
],
"status":"in-review",
"isApplicationPageActive":false,
"createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c",
"updatedBy":"00000000-0000-0000-0000-000000000000",
"createdAt":"2021-05-09T21:21:10.394Z",
"updatedAt":"2021-05-09T21:21:14.010Z"
},
"ResourceBooking":{
"id":"41671764-0ded-46fd-b7de-2af5d5e4f3fc",
"projectId":17234,
"userId":"05e988b7-7d54-4c10-ada1-1a04870a88a8",
"jobId":"43d695d4-e926-41d5-ad42-a899612b5246",
"status":"placed",
"startDate":"2020-09-27",
"endDate":"2020-10-27",
"memberRate":13.23,
"customerRate":13,
"rateType":"hourly",
"billingAccountId":80000069,
"createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c",
"updatedBy":null,
"createdAt":"2021-05-09T21:25:46.728Z",
"updatedAt":"2021-05-09T21:25:46.728Z"
},
"WorkPeriods":[
{
"id":"4baae2cf-fd70-4ab3-9959-e826257b7e0f",
"resourceBookingId":"41671764-0ded-46fd-b7de-2af5d5e4f3fc",
"userHandle":"pshah_manager",
"projectId":17234,
"startDate":"2020-09-27",
"endDate":"2020-10-03",
"daysWorked":4,
"memberRate":27.06,
"customerRate":13.13,
"paymentStatus":"partially-completed",
"createdBy":"00000000-0000-0000-0000-000000000000",
"updatedBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c",
"createdAt":"2021-05-09T21:25:47.813Z",
"updatedAt":"2021-05-09T21:45:32.659Z"
},
{
"id":"9918e1b7-acbc-41ae-baa6-fdcb2386681d",
"resourceBookingId":"41671764-0ded-46fd-b7de-2af5d5e4f3fc",
"userHandle":"Shuchikr",
"projectId":17234,
"startDate":"2020-10-18",
"endDate":"2020-10-24",
"daysWorked":4,
"memberRate":4.08,
"customerRate":3.89,
"paymentStatus":"cancelled",
"createdBy":"00000000-0000-0000-0000-000000000000",
"updatedBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c",
"createdAt":"2021-05-09T21:25:47.834Z",
"updatedAt":"2021-05-09T21:45:37.647Z"
},
{
"id":"42e990c9-b14c-4496-9977-c3024aa90024",
"resourceBookingId":"41671764-0ded-46fd-b7de-2af5d5e4f3fc",
"userHandle":"vkumars",
"projectId":17234,
"startDate":"2020-10-25",
"endDate":"2020-10-31",
"daysWorked":3,
"memberRate":15.61,
"customerRate":9.76,
"paymentStatus":"pending",
"createdBy":"00000000-0000-0000-0000-000000000000",
"updatedBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c",
"createdAt":"2021-05-09T21:25:47.824Z",
"updatedAt":"2021-05-09T21:45:48.727Z"
},
{
"id":"8bf64481-ae7b-4e51-b48c-000cd90c87d1",
"resourceBookingId":"41671764-0ded-46fd-b7de-2af5d5e4f3fc",
"userHandle":"chandanant",
"projectId":17234,
"startDate":"2020-10-11",
"endDate":"2020-10-17",
"daysWorked":4,
"memberRate":10.82,
"customerRate":30.71,
"paymentStatus":"pending",
"createdBy":"00000000-0000-0000-0000-000000000000",
"updatedBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c",
"createdAt":"2021-05-09T21:25:47.815Z",
"updatedAt":"2021-05-09T21:45:41.810Z"
}
]
}
Loading