Skip to content

Commit 67c82a7

Browse files
committed
Batch Payments - Part 1 - Scheduler
1 parent 232329c commit 67c82a7

20 files changed

+938
-40
lines changed

README.md

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

app-constants.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,14 @@ const ChallengeStatus = {
7676
COMPLETED: 'Completed'
7777
}
7878

79+
const WorkPeriodPaymentStatus = {
80+
COMPLETED: 'completed',
81+
SCHEDULED: 'scheduled',
82+
IN_PROGRESS: 'in-progress',
83+
FAILED: 'failed',
84+
CANCELLED: 'cancelled'
85+
}
86+
7987
const PaymentProcessingSwitch = {
8088
ON: 'ON',
8189
OFF: 'OFF'
@@ -87,5 +95,6 @@ module.exports = {
8795
Scopes,
8896
Interviews,
8997
ChallengeStatus,
98+
WorkPeriodPaymentStatus,
9099
PaymentProcessingSwitch
91100
}

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: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,5 +162,38 @@ module.exports = {
162162
DEFAULT_TIMELINE_TEMPLATE_ID: process.env.DEFAULT_TIMELINE_TEMPLATE_ID || '53a307ce-b4b3-4d6f-b9a1-3741a58f77e6',
163163
DEFAULT_TRACK_ID: process.env.DEFAULT_TRACK_ID || '9b6fc876-f4d9-4ccb-9dfd-419247628825',
164164

165-
PAYMENT_PROCESSING_SWITCH: process.env.PAYMENT_PROCESSING_SWITCH || 'OFF'
165+
PAYMENT_PROCESSING: {
166+
// switch off actual API calls in Payment Scheduler
167+
SWITCH: process.env.PAYMENT_PROCESSING_SWITCH || 'OFF',
168+
// the payment scheduler cron config
169+
CRON: process.env.PAYMENT_PROCESSING_CRON || '0 */5 * * * *',
170+
// the number of records processed by one time
171+
BATCH_SIZE: parseInt(process.env.PAYMENT_PROCESSING_BATCH_SIZE || 50),
172+
// in-progress expired to determine whether a record has been processed abnormally, moment duration format
173+
IN_PROGRESS_EXPIRED: process.env.IN_PROGRESS_EXPIRED || 'PT1H',
174+
// the number of max retry config
175+
MAX_RETRY_COUNT: parseInt(process.env.PAYMENT_PROCESSING_MAX_RETRY_COUNT || 10),
176+
// the time of retry base delay, unit: ms
177+
RETRY_BASE_DELAY: parseInt(process.env.PAYMENT_PROCESSING_RETRY_BASE_DELAY || 100),
178+
// the time of retry max delay, unit: ms
179+
RETRY_MAX_DELAY: parseInt(process.env.PAYMENT_PROCESSING_RETRY_MAX_DELAY || 10000),
180+
// the max time of one request, unit: ms
181+
PER_REQUEST_MAX_TIME: parseInt(process.env.PAYMENT_PROCESSING_PER_REQUEST_MAX_TIME || 30000),
182+
// the max time of one payment record, unit: ms
183+
PER_PAYMENT_MAX_TIME: parseInt(process.env.PAYMENT_PROCESSING_PER_PAYMENT_MAX_TIME || 60000),
184+
// the max records of payment of a minute
185+
PER_MINUTE_PAYMENT_MAX_COUNT: parseInt(process.env.PAYMENT_PROCESSING_PER_MINUTE_PAYMENT_MAX_COUNT || 12),
186+
// the max requests of challenge of a minute
187+
PER_MINUTE_CHALLENGE_REQUEST_MAX_COUNT: parseInt(process.env.PAYMENT_PROCESSING_PER_MINUTE_CHALLENGE_REQUEST_MAX_COUNT || 60),
188+
// the max requests of resource of a minute
189+
PER_MINUTE_RESOURCE_REQUEST_MAX_COUNT: parseInt(process.env.PAYMENT_PROCESSING_PER_MINUTE_CHALLENGE_REQUEST_MAX_COUNT || 20),
190+
// the default step fix delay, unit: ms
191+
FIX_DELAY_STEP: parseInt(process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP || 500),
192+
// the fix delay between step one and step two, unit: ms
193+
FIX_DELAY_STEP_1_2: parseInt(process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP_1_2 || process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP || 500),
194+
// the fix delay between step two and step three, unit: ms
195+
FIX_DELAY_STEP_2_3: parseInt(process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP_2_3 || process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP || 500),
196+
// the fix delay between step three and step four, unit: ms
197+
FIX_DELAY_STEP_3_4: parseInt(process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP_3_4 || process.env.PAYMENT_PROCESSING_FIX_DELAY_STEP || 500)
198+
}
166199
}

data/demo-data.json

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -182,12 +182,12 @@
182182
{
183183
"id": "077aa2ca-5b60-4ad9-a965-1b37e08a5046",
184184
"jobCandidateId": "881a19de-2b0c-4bb9-b36a-4cb5e223bdb5",
185-
"googleCalendarId": null,
185+
"calendarEventId": null,
186186
"customMessage": null,
187-
"xaiTemplate": "interview-30",
187+
"templateUrl": "interview-30",
188188
"round": 1,
189189
"startTimestamp": null,
190-
"attendeesList": null,
190+
"guestEmails": null,
191191
"status": "Completed",
192192
"createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c",
193193
"updatedBy": null,
@@ -211,12 +211,12 @@
211211
{
212212
"id": "b1f7ba76-640f-47e2-9463-59e51b51ec60",
213213
"jobCandidateId": "827ee401-df04-42e1-abbe-7b97ce7937ff",
214-
"googleCalendarId": "dummyId",
214+
"calendarEventId": "dummyId",
215215
"customMessage": "This is a custom message",
216-
"xaiTemplate": "interview-30",
216+
"templateUrl": "interview-30",
217217
"round": 2,
218218
"startTimestamp": null,
219-
"attendeesList": null,
219+
"guestEmails": null,
220220
"status": "Scheduling",
221221
"createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c",
222222
"updatedBy": null,
@@ -226,12 +226,12 @@
226226
{
227227
"id": "3144fa65-ea1a-4bec-81b0-7cb1c8845826",
228228
"jobCandidateId": "827ee401-df04-42e1-abbe-7b97ce7937ff",
229-
"googleCalendarId": null,
229+
"calendarEventId": null,
230230
"customMessage": null,
231-
"xaiTemplate": "interview-30",
231+
"templateUrl": "interview-30",
232232
"round": 1,
233233
"startTimestamp": null,
234-
"attendeesList": null,
234+
"guestEmails": null,
235235
"status": "Completed",
236236
"createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c",
237237
"updatedBy": null,
@@ -255,12 +255,12 @@
255255
{
256256
"id": "976d23a9-5710-453f-99d9-f57a588bb610",
257257
"jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36",
258-
"googleCalendarId": "dummyId",
258+
"calendarEventId": "dummyId",
259259
"customMessage": "This is a custom message",
260-
"xaiTemplate": "interview-30",
260+
"templateUrl": "interview-30",
261261
"round": 3,
262262
"startTimestamp": null,
263-
"attendeesList": [
263+
"guestEmails": [
264264
"attendee1@yopmail.com",
265265
"attendee2@yopmail.com"
266266
],
@@ -273,12 +273,12 @@
273273
{
274274
"id": "a23e1bf2-1084-4cfe-a0d8-d83bc6fec655",
275275
"jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36",
276-
"googleCalendarId": "dummyId",
276+
"calendarEventId": "dummyId",
277277
"customMessage": "This is a custom message",
278-
"xaiTemplate": "interview-30",
278+
"templateUrl": "interview-30",
279279
"round": 2,
280280
"startTimestamp": null,
281-
"attendeesList": [
281+
"guestEmails": [
282282
"attendee1@yopmail.com",
283283
"attendee2@yopmail.com"
284284
],
@@ -291,12 +291,12 @@
291291
{
292292
"id": "9efd72c3-1dc7-4ce2-9869-8cca81d0adeb",
293293
"jobCandidateId": "a4ea7bcf-5b99-4381-b99c-a9bd05d83a36",
294-
"googleCalendarId": null,
294+
"calendarEventId": null,
295295
"customMessage": null,
296-
"xaiTemplate": "interview-30",
296+
"templateUrl": "interview-30",
297297
"round": 1,
298298
"startTimestamp": null,
299-
"attendeesList": null,
299+
"guestEmails": null,
300300
"status": "Completed",
301301
"createdBy": "57646ff9-1cd3-4d3c-88ba-eb09a395366c",
302302
"updatedBy": null,

docs/swagger.yaml

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2380,7 +2380,7 @@ paths:
23802380
required: false
23812381
schema:
23822382
type: string
2383-
enum: ["completed", "cancelled"]
2383+
enum: ["completed", "scheduled", "in-progress", "failed", "cancelled"]
23842384
description: The payment status.
23852385
responses:
23862386
"200":
@@ -4211,8 +4211,22 @@ components:
42114211
description: "The amount to be paid."
42124212
status:
42134213
type: string
4214-
enum: ["completed", "cancelled"]
4214+
enum: ["completed", "scheduled", "in-progress", "failed", "cancelled"]
42154215
description: "The payment status."
4216+
statusDetails:
4217+
type: object
4218+
properties:
4219+
errorMessage:
4220+
type: string
4221+
errorCode:
4222+
type: integer
4223+
retry:
4224+
type: integer
4225+
step:
4226+
type: string
4227+
challengeId:
4228+
type: string
4229+
format: uuid
42164230
billingAccountId:
42174231
type: integer
42184232
example: 80000071
@@ -4247,7 +4261,7 @@ components:
42474261
description: "The amount to be paid."
42484262
status:
42494263
type: string
4250-
enum: ["completed", "cancelled"]
4264+
enum: ["completed", "scheduled", "in-progress", "failed", "cancelled"]
42514265
description: "The payment status."
42524266
WorkPeriodPaymentPatchRequestBody:
42534267
properties:
@@ -4261,7 +4275,7 @@ components:
42614275
description: "The amount to be paid."
42624276
status:
42634277
type: string
4264-
enum: ["completed", "cancelled"]
4278+
enum: ["completed", "scheduled", "in-progress", "failed", "cancelled"]
42654279
description: "The payment status."
42664280
CheckRun:
42674281
type: object
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
'use strict';
2+
3+
const config = require('config')
4+
const _ = require('lodash')
5+
6+
/**
7+
* Create `payment_schedulers` table & relations.
8+
*/
9+
module.exports = {
10+
up: async (queryInterface, Sequelize) => {
11+
const transaction = await queryInterface.sequelize.transaction()
12+
try {
13+
await queryInterface.createTable('payment_schedulers', {
14+
id: {
15+
type: Sequelize.UUID,
16+
primaryKey: true,
17+
allowNull: false,
18+
defaultValue: Sequelize.UUIDV4
19+
},
20+
challengeId: {
21+
field: 'challenge_id',
22+
type: Sequelize.UUID,
23+
allowNull: false
24+
},
25+
workPeriodPaymentId: {
26+
field: 'work_period_payment_id',
27+
type: Sequelize.UUID,
28+
allowNull: false,
29+
references: {
30+
model: {
31+
tableName: 'work_period_payments',
32+
schema: config.DB_SCHEMA_NAME
33+
},
34+
key: 'id'
35+
}
36+
},
37+
step: {
38+
type: Sequelize.INTEGER,
39+
allowNull: false
40+
},
41+
status: {
42+
type: Sequelize.ENUM(
43+
'in-progress',
44+
'completed',
45+
'failed'
46+
),
47+
allowNull: false
48+
},
49+
userId: {
50+
field: 'user_id',
51+
type: Sequelize.BIGINT
52+
},
53+
userHandle: {
54+
field: 'user_handle',
55+
type: Sequelize.STRING,
56+
allowNull: false
57+
},
58+
createdAt: {
59+
field: 'created_at',
60+
type: Sequelize.DATE
61+
},
62+
updatedAt: {
63+
field: 'updated_at',
64+
type: Sequelize.DATE
65+
},
66+
deletedAt: {
67+
field: 'deleted_at',
68+
type: Sequelize.DATE
69+
}
70+
}, { schema: config.DB_SCHEMA_NAME, transaction })
71+
await queryInterface.addColumn({ tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME }, 'status_details',
72+
{ type: Sequelize.JSONB },
73+
{ transaction })
74+
await queryInterface.changeColumn({ tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME }, 'challenge_id',
75+
{ type: Sequelize.UUID },
76+
{ transaction })
77+
await queryInterface.sequelize.query(`ALTER TYPE ${config.DB_SCHEMA_NAME}.enum_work_period_payments_status ADD VALUE 'scheduled'`)
78+
await queryInterface.sequelize.query(`ALTER TYPE ${config.DB_SCHEMA_NAME}.enum_work_period_payments_status ADD VALUE 'in-progress'`)
79+
await queryInterface.sequelize.query(`ALTER TYPE ${config.DB_SCHEMA_NAME}.enum_work_period_payments_status ADD VALUE 'failed'`)
80+
await transaction.commit()
81+
} catch (err) {
82+
await transaction.rollback()
83+
throw err
84+
}
85+
},
86+
87+
down: async (queryInterface, Sequelize) => {
88+
const table = { schema: config.DB_SCHEMA_NAME, tableName: 'payment_schedulers' }
89+
const statusTypeName = `${table.schema}.enum_${table.tableName}_status`
90+
const transaction = await queryInterface.sequelize.transaction()
91+
try {
92+
await queryInterface.dropTable(table, { transaction })
93+
// drop enum type for status column
94+
await queryInterface.sequelize.query(`DROP TYPE ${statusTypeName}`, { transaction })
95+
96+
await queryInterface.changeColumn({ tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME }, 'challenge_id',
97+
{ type: Sequelize.UUID, allowNull: false },
98+
{ transaction })
99+
await queryInterface.removeColumn({ tableName: 'work_period_payments', schema: config.DB_SCHEMA_NAME }, 'status_details',
100+
{ transaction })
101+
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')`,
102+
{ transaction })
103+
await transaction.commit()
104+
} catch (err) {
105+
await transaction.rollback()
106+
throw err
107+
}
108+
}
109+
};

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"local:init": "npm run local:reset && npm run data:import -- --force",
2727
"local:reset": "npm run delete-index -- --force || true && npm run create-index -- --force && npm run init-db force",
2828
"cov": "nyc --reporter=html --reporter=text npm run test",
29+
"demo-payment-scheduler": "node scripts/demo-payment-scheduler/index.js && npm run index:all -- --force",
2930
"demo-payment": "node scripts/demo-payment"
3031
},
3132
"keywords": [],

0 commit comments

Comments
 (0)