Skip to content

Commit 705ef9d

Browse files
authored
Merge pull request #318 from topcoder-platform/feature/payment-scheduler
Payment scheduler
2 parents 7c67759 + 9d4d436 commit 705ef9d

15 files changed

+943
-14
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ To be able to change and test `taas-es-processor` locally you can follow the nex
207207
| `npm run cov` | Code Coverage Report. |
208208
| `npm run migrate` | Run any migration files which haven't run yet. |
209209
| `npm run migrate:undo` | Revert most recent migration. |
210+
| `npm run demo-payment-scheduler` | Create 1000 Work Periods Payment records in with status "scheduled" and various "amount" |
210211
211212
## Import and Export data
212213

app-constants.js

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,24 @@ const ChallengeStatus = {
8686

8787
const WorkPeriodPaymentStatus = {
8888
COMPLETED: 'completed',
89-
CANCELLED: 'cancelled',
90-
SCHEDULED: 'scheduled'
89+
SCHEDULED: 'scheduled',
90+
IN_PROGRESS: 'in-progress',
91+
FAILED: 'failed',
92+
CANCELLED: 'cancelled'
93+
}
94+
95+
const PaymentProcessingSwitch = {
96+
ON: 'ON',
97+
OFF: 'OFF'
98+
}
99+
100+
const PaymentSchedulerStatus = {
101+
START_PROCESS: 'start-process',
102+
CREATE_CHALLENGE: 'create-challenge',
103+
ASSIGN_MEMBER: 'assign-member',
104+
ACTIVATE_CHALLENGE: 'activate-challenge',
105+
GET_USER_ID: 'get-userId',
106+
CLOSE_CHALLENGE: 'close-challenge'
91107
}
92108

93109
module.exports = {
@@ -96,5 +112,7 @@ module.exports = {
96112
Scopes,
97113
Interviews,
98114
ChallengeStatus,
99-
WorkPeriodPaymentStatus
115+
WorkPeriodPaymentStatus,
116+
PaymentSchedulerStatus,
117+
PaymentProcessingSwitch
100118
}

app.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const schedule = require('node-schedule')
1313
const logger = require('./src/common/logger')
1414
const eventHandlers = require('./src/eventHandlers')
1515
const interviewService = require('./src/services/InterviewService')
16+
const { processScheduler } = require('./src/services/PaymentSchedulerService')
1617

1718
// setup express app
1819
const app = express()
@@ -97,6 +98,9 @@ const server = app.listen(app.get('port'), () => {
9798
eventHandlers.init()
9899
// schedule updateCompletedInterviews to run every hour
99100
schedule.scheduleJob('0 0 * * * *', interviewService.updateCompletedInterviews)
101+
102+
// schedule payment processing
103+
schedule.scheduleJob(config.PAYMENT_PROCESSING.CRON, processScheduler)
100104
})
101105

102106
if (process.env.NODE_ENV === 'test') {

config/default.js

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,5 +175,40 @@ module.exports = {
175175
// the minimum matching rate when searching roles by skills
176176
ROLE_MATCHING_RATE: process.env.ROLE_MATCHING_RATE || 0.70,
177177
// member groups representing Wipro or TopCoder employee
178-
INTERNAL_MEMBER_GROUPS: process.env.INTERNAL_MEMBER_GROUPS || ['20000000', '20000001', '20000003', '20000010', '20000015']
178+
INTERNAL_MEMBER_GROUPS: process.env.INTERNAL_MEMBER_GROUPS || ['20000000', '20000001', '20000003', '20000010', '20000015'],
179+
// payment scheduler config
180+
PAYMENT_PROCESSING: {
181+
// switch off actual API calls in Payment Scheduler
182+
SWITCH: process.env.PAYMENT_PROCESSING_SWITCH || 'OFF',
183+
// the payment scheduler cron config
184+
CRON: process.env.PAYMENT_PROCESSING_CRON || '0 */5 * * * *',
185+
// the number of records processed by one time
186+
BATCH_SIZE: parseInt(process.env.PAYMENT_PROCESSING_BATCH_SIZE || 50),
187+
// in-progress expired to determine whether a record has been processed abnormally, moment duration format
188+
IN_PROGRESS_EXPIRED: process.env.IN_PROGRESS_EXPIRED || 'PT1H',
189+
// the number of max retry config
190+
MAX_RETRY_COUNT: parseInt(process.env.PAYMENT_PROCESSING_MAX_RETRY_COUNT || 10),
191+
// the time of retry base delay, unit: ms
192+
RETRY_BASE_DELAY: parseInt(process.env.PAYMENT_PROCESSING_RETRY_BASE_DELAY || 100),
193+
// the time of retry max delay, unit: ms
194+
RETRY_MAX_DELAY: parseInt(process.env.PAYMENT_PROCESSING_RETRY_MAX_DELAY || 10000),
195+
// the max time of one request, unit: ms
196+
PER_REQUEST_MAX_TIME: parseInt(process.env.PAYMENT_PROCESSING_PER_REQUEST_MAX_TIME || 30000),
197+
// the max time of one payment record, unit: ms
198+
PER_PAYMENT_MAX_TIME: parseInt(process.env.PAYMENT_PROCESSING_PER_PAYMENT_MAX_TIME || 60000),
199+
// the max records of payment of a minute
200+
PER_MINUTE_PAYMENT_MAX_COUNT: parseInt(process.env.PAYMENT_PROCESSING_PER_MINUTE_PAYMENT_MAX_COUNT || 12),
201+
// the max requests of challenge of a minute
202+
PER_MINUTE_CHALLENGE_REQUEST_MAX_COUNT: parseInt(process.env.PAYMENT_PROCESSING_PER_MINUTE_CHALLENGE_REQUEST_MAX_COUNT || 60),
203+
// the max requests of resource of a minute
204+
PER_MINUTE_RESOURCE_REQUEST_MAX_COUNT: parseInt(process.env.PAYMENT_PROCESSING_PER_MINUTE_CHALLENGE_REQUEST_MAX_COUNT || 20),
205+
// the default step fix delay, unit: ms
206+
FIX_DELAY_STEP: parseInt(process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP || 500),
207+
// the fix delay after step of create challenge, unit: ms
208+
FIX_DELAY_STEP_CREATE_CHALLENGE: parseInt(process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP_CREATE_CHALLENGE || process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP || 500),
209+
// the fix delay after step of assign member, unit: ms
210+
FIX_DELAY_STEP_ASSIGN_MEMBER: parseInt(process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP_ASSIGN_MEMBER || process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP || 500),
211+
// the fix delay after step of activate challenge, unit: ms
212+
FIX_DELAY_STEP_ACTIVATE_CHALLENGE: parseInt(process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP_ACTIVATE_CHALLENGE || process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP || 500)
213+
}
179214
}

docs/swagger.yaml

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2403,7 +2403,7 @@ paths:
24032403
required: false
24042404
schema:
24052405
type: string
2406-
enum: ["completed", "scheduled", "cancelled"]
2406+
enum: ["completed", "scheduled", "in-progress", "failed", "cancelled"]
24072407
description: The payment status.
24082408
responses:
24092409
"200":
@@ -2519,7 +2519,7 @@ paths:
25192519
application/json:
25202520
schema:
25212521
$ref: "#/components/schemas/Error"
2522-
2522+
25232523
/work-period-payments/{id}:
25242524
get:
25252525
tags:
@@ -4830,8 +4830,22 @@ components:
48304830
description: "The amount to be paid."
48314831
status:
48324832
type: string
4833-
enum: ["completed", "scheduled", "cancelled"]
4833+
enum: ["completed", "scheduled", "in-progress", "failed", "cancelled"]
48344834
description: "The payment status."
4835+
statusDetails:
4836+
type: object
4837+
properties:
4838+
errorMessage:
4839+
type: string
4840+
errorCode:
4841+
type: integer
4842+
retry:
4843+
type: integer
4844+
step:
4845+
type: string
4846+
challengeId:
4847+
type: string
4848+
format: uuid
48354849
billingAccountId:
48364850
type: integer
48374851
example: 80000071
@@ -4888,7 +4902,7 @@ components:
48884902
description: "The amount to be paid."
48894903
status:
48904904
type: string
4891-
enum: ["completed", "scheduled", "cancelled"]
4905+
enum: ["completed", "scheduled", "in-progress", "failed", "cancelled"]
48924906
description: "The payment status."
48934907
WorkPeriodPaymentCreateRequestBody:
48944908
required:
@@ -4979,7 +4993,7 @@ components:
49794993
description: "The amount to be paid."
49804994
status:
49814995
type: string
4982-
enum: ["completed", "scheduled", "cancelled"]
4996+
enum: ["completed", "scheduled", "in-progress", "failed", "cancelled"]
49834997
description: "The payment status."
49844998
CheckRun:
49854999
type: object
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
'use strict';
2+
3+
const config = require('config')
4+
const _ = require('lodash')
5+
const { PaymentSchedulerStatus } = require('../app-constants')
6+
7+
/**
8+
* Create `payment_schedulers` table & relations.
9+
*/
10+
module.exports = {
11+
up: async (queryInterface, Sequelize) => {
12+
const transaction = await queryInterface.sequelize.transaction()
13+
try {
14+
await queryInterface.createTable('payment_schedulers', {
15+
id: {
16+
type: Sequelize.UUID,
17+
primaryKey: true,
18+
allowNull: false,
19+
defaultValue: Sequelize.UUIDV4
20+
},
21+
challengeId: {
22+
field: 'challenge_id',
23+
type: Sequelize.UUID,
24+
allowNull: false
25+
},
26+
workPeriodPaymentId: {
27+
field: 'work_period_payment_id',
28+
type: Sequelize.UUID,
29+
allowNull: false,
30+
references: {
31+
model: {
32+
tableName: 'work_period_payments',
33+
schema: config.DB_SCHEMA_NAME
34+
},
35+
key: 'id'
36+
}
37+
},
38+
step: {
39+
type: Sequelize.ENUM(_.values(PaymentSchedulerStatus)),
40+
allowNull: false
41+
},
42+
status: {
43+
type: Sequelize.ENUM(
44+
'in-progress',
45+
'completed',
46+
'failed'
47+
),
48+
allowNull: false
49+
},
50+
userId: {
51+
field: 'user_id',
52+
type: Sequelize.BIGINT
53+
},
54+
userHandle: {
55+
field: 'user_handle',
56+
type: Sequelize.STRING,
57+
allowNull: false
58+
},
59+
createdAt: {
60+
field: 'created_at',
61+
type: Sequelize.DATE
62+
},
63+
updatedAt: {
64+
field: 'updated_at',
65+
type: Sequelize.DATE
66+
},
67+
deletedAt: {
68+
field: 'deleted_at',
69+
type: Sequelize.DATE
70+
}
71+
}, { schema: config.DB_SCHEMA_NAME, transaction })
72+
await queryInterface.addColumn({ tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME }, 'status_details',
73+
{ type: Sequelize.JSONB },
74+
{ transaction })
75+
await queryInterface.changeColumn({ tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME }, 'challenge_id',
76+
{ type: Sequelize.UUID },
77+
{ transaction })
78+
await queryInterface.sequelize.query(`ALTER TYPE ${config.DB_SCHEMA_NAME}.enum_work_period_payments_status ADD VALUE 'scheduled'`)
79+
await queryInterface.sequelize.query(`ALTER TYPE ${config.DB_SCHEMA_NAME}.enum_work_period_payments_status ADD VALUE 'in-progress'`)
80+
await queryInterface.sequelize.query(`ALTER TYPE ${config.DB_SCHEMA_NAME}.enum_work_period_payments_status ADD VALUE 'failed'`)
81+
await transaction.commit()
82+
} catch (err) {
83+
await transaction.rollback()
84+
throw err
85+
}
86+
},
87+
88+
down: async (queryInterface, Sequelize) => {
89+
const table = { schema: config.DB_SCHEMA_NAME, tableName: 'payment_schedulers' }
90+
const statusTypeName = `${table.schema}.enum_${table.tableName}_status`
91+
const stepTypeName = `${table.schema}.enum_${table.tableName}_step`
92+
const transaction = await queryInterface.sequelize.transaction()
93+
try {
94+
await queryInterface.dropTable(table, { transaction })
95+
// drop enum type for status and step column
96+
await queryInterface.sequelize.query(`DROP TYPE ${statusTypeName}`, { transaction })
97+
await queryInterface.sequelize.query(`DROP TYPE ${stepTypeName}`, { transaction })
98+
99+
await queryInterface.changeColumn({ tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME }, 'challenge_id',
100+
{ type: Sequelize.UUID, allowNull: false },
101+
{ transaction })
102+
await queryInterface.removeColumn({ tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME }, 'status_details',
103+
{ transaction })
104+
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')`,
105+
{ transaction })
106+
await transaction.commit()
107+
} catch (err) {
108+
await transaction.rollback()
109+
throw err
110+
}
111+
}
112+
};

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"local:init": "npm run local:reset && npm run data:import -- --force",
2828
"local:reset": "npm run delete-index -- --force || true && npm run create-index -- --force && npm run init-db force",
2929
"cov": "nyc --reporter=html --reporter=text npm run test",
30+
"demo-payment-scheduler": "node scripts/demo-payment-scheduler/index.js && npm run index:all -- --force",
3031
"demo-payment": "node scripts/demo-payment"
3132
},
3233
"keywords": [],
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
{
2+
"Job":{
3+
"id":"43d695d4-e926-41d5-ad42-a899612b5246",
4+
"projectId":17234,
5+
"title":"Dummy title - at most 64 characters",
6+
"numPositions":13,
7+
"skills":[
8+
"23e00d92-207a-4b5b-b3c9-4c5662644941",
9+
"7d076384-ccf6-4e43-a45d-1b24b1e624aa",
10+
"cbac57a3-7180-4316-8769-73af64893158",
11+
"a2b4bc11-c641-4a19-9eb7-33980378f82e"
12+
],
13+
"status":"in-review",
14+
"isApplicationPageActive":false,
15+
"createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c",
16+
"updatedBy":"00000000-0000-0000-0000-000000000000",
17+
"createdAt":"2021-05-09T21:21:10.394Z",
18+
"updatedAt":"2021-05-09T21:21:14.010Z"
19+
},
20+
"ResourceBooking":{
21+
"id":"41671764-0ded-46fd-b7de-2af5d5e4f3fc",
22+
"projectId":17234,
23+
"userId":"05e988b7-7d54-4c10-ada1-1a04870a88a8",
24+
"jobId":"43d695d4-e926-41d5-ad42-a899612b5246",
25+
"status":"placed",
26+
"startDate":"2020-09-27",
27+
"endDate":"2020-10-27",
28+
"memberRate":13.23,
29+
"customerRate":13,
30+
"rateType":"hourly",
31+
"billingAccountId":80000069,
32+
"createdBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c",
33+
"updatedBy":null,
34+
"createdAt":"2021-05-09T21:25:46.728Z",
35+
"updatedAt":"2021-05-09T21:25:46.728Z"
36+
},
37+
"WorkPeriods":[
38+
{
39+
"id":"4baae2cf-fd70-4ab3-9959-e826257b7e0f",
40+
"resourceBookingId":"41671764-0ded-46fd-b7de-2af5d5e4f3fc",
41+
"userHandle":"pshah_manager",
42+
"projectId":17234,
43+
"startDate":"2020-09-27",
44+
"endDate":"2020-10-03",
45+
"daysWorked":4,
46+
"memberRate":27.06,
47+
"customerRate":13.13,
48+
"paymentStatus":"partially-completed",
49+
"createdBy":"00000000-0000-0000-0000-000000000000",
50+
"updatedBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c",
51+
"createdAt":"2021-05-09T21:25:47.813Z",
52+
"updatedAt":"2021-05-09T21:45:32.659Z"
53+
},
54+
{
55+
"id":"9918e1b7-acbc-41ae-baa6-fdcb2386681d",
56+
"resourceBookingId":"41671764-0ded-46fd-b7de-2af5d5e4f3fc",
57+
"userHandle":"Shuchikr",
58+
"projectId":17234,
59+
"startDate":"2020-10-18",
60+
"endDate":"2020-10-24",
61+
"daysWorked":4,
62+
"memberRate":4.08,
63+
"customerRate":3.89,
64+
"paymentStatus":"cancelled",
65+
"createdBy":"00000000-0000-0000-0000-000000000000",
66+
"updatedBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c",
67+
"createdAt":"2021-05-09T21:25:47.834Z",
68+
"updatedAt":"2021-05-09T21:45:37.647Z"
69+
},
70+
{
71+
"id":"42e990c9-b14c-4496-9977-c3024aa90024",
72+
"resourceBookingId":"41671764-0ded-46fd-b7de-2af5d5e4f3fc",
73+
"userHandle":"vkumars",
74+
"projectId":17234,
75+
"startDate":"2020-10-25",
76+
"endDate":"2020-10-31",
77+
"daysWorked":3,
78+
"memberRate":15.61,
79+
"customerRate":9.76,
80+
"paymentStatus":"pending",
81+
"createdBy":"00000000-0000-0000-0000-000000000000",
82+
"updatedBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c",
83+
"createdAt":"2021-05-09T21:25:47.824Z",
84+
"updatedAt":"2021-05-09T21:45:48.727Z"
85+
},
86+
{
87+
"id":"8bf64481-ae7b-4e51-b48c-000cd90c87d1",
88+
"resourceBookingId":"41671764-0ded-46fd-b7de-2af5d5e4f3fc",
89+
"userHandle":"chandanant",
90+
"projectId":17234,
91+
"startDate":"2020-10-11",
92+
"endDate":"2020-10-17",
93+
"daysWorked":4,
94+
"memberRate":10.82,
95+
"customerRate":30.71,
96+
"paymentStatus":"pending",
97+
"createdBy":"00000000-0000-0000-0000-000000000000",
98+
"updatedBy":"57646ff9-1cd3-4d3c-88ba-eb09a395366c",
99+
"createdAt":"2021-05-09T21:25:47.815Z",
100+
"updatedAt":"2021-05-09T21:45:41.810Z"
101+
}
102+
]
103+
}

0 commit comments

Comments
 (0)