Skip to content

Commit 9beb3a7

Browse files
Merge pull request #217 from imcaizheng/feature/use-billing-account-for-payments
Use billing account for payments
2 parents 06216d2 + 12ed3c5 commit 9beb3a7

11 files changed

+175
-63
lines changed

docs/Topcoder-bookings-api.postman_collection.json

Lines changed: 40 additions & 40 deletions
Large diffs are not rendered by default.

docs/swagger.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3162,6 +3162,10 @@ components:
31623162
type: string
31633163
enum: ["hourly", "daily", "weekly", "monthly"]
31643164
description: "The rate type of the job."
3165+
billingAccountId:
3166+
type: integer
3167+
example: 80000071
3168+
description: 'the billing account id for payments'
31653169
createdAt:
31663170
type: string
31673171
format: date-time
@@ -3226,6 +3230,10 @@ components:
32263230
type: string
32273231
enum: ["hourly", "daily", "weekly", "monthly"]
32283232
description: "The rate type of the job."
3233+
billingAccountId:
3234+
type: integer
3235+
example: 80000071
3236+
description: 'the billing account id for payments'
32293237
ResourceBookingPatchRequestBody:
32303238
properties:
32313239
status:
@@ -3255,6 +3263,10 @@ components:
32553263
type: string
32563264
enum: ["hourly", "daily", "weekly", "monthly"]
32573265
description: "The rate type of the job."
3266+
billingAccountId:
3267+
type: integer
3268+
example: 80000071
3269+
description: 'the billing account id for payments'
32583270
WorkPeriod:
32593271
required:
32603272
- id
@@ -3435,6 +3447,10 @@ components:
34353447
type: string
34363448
enum: ["completed", "cancelled"]
34373449
description: "The payment status."
3450+
billingAccountId:
3451+
type: integer
3452+
example: 80000071
3453+
description: 'the billing account id for payments'
34383454
createdAt:
34393455
type: string
34403456
format: date-time
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
const config = require('config')
2+
3+
/*
4+
* Add billingAccountId field to the ResourceBooking and WorkPeriodPayment models.
5+
*/
6+
7+
module.exports = {
8+
up: async (queryInterface, Sequelize) => {
9+
const transaction = await queryInterface.sequelize.transaction()
10+
try {
11+
for (const tableName of ['resource_bookings', 'work_period_payments']) {
12+
await queryInterface.addColumn({ tableName, schema: config.DB_SCHEMA_NAME }, 'billing_account_id',
13+
{ type: Sequelize.BIGINT },
14+
{ transaction })
15+
}
16+
await transaction.commit()
17+
} catch (err) {
18+
await transaction.rollback()
19+
throw err
20+
}
21+
},
22+
down: async (queryInterface, Sequelize) => {
23+
for (const tableName of ['resource_bookings', 'work_period_payments']) {
24+
await queryInterface.removeColumn({ tableName, schema: config.DB_SCHEMA_NAME }, 'billing_account_id')
25+
}
26+
}
27+
}

src/common/errors.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,6 @@ module.exports = {
3535
UnauthorizedError: createError('UnauthorizedError', 401),
3636
ForbiddenError: createError('ForbiddenError', 403),
3737
NotFoundError: createError('NotFoundError', 404),
38+
ConflictError: createError('ConflictError', 409),
3839
InternalServerError: createError('InternalServerError', 500)
3940
}

src/common/helper.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = {
9898
memberRate: { type: 'float' },
9999
customerRate: { type: 'float' },
100100
rateType: { type: 'keyword' },
101+
billingAccountId: { type: 'integer' },
101102
createdAt: { type: 'date' },
102103
createdBy: { type: 'keyword' },
103104
updatedAt: { type: 'date' },
@@ -122,6 +123,7 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_WORK_PERIOD')] = {
122123
challengeId: { type: 'keyword' },
123124
amount: { type: 'float' },
124125
status: { type: 'keyword' },
126+
billingAccountId: { type: 'integer' },
125127
createdAt: { type: 'date' },
126128
createdBy: { type: 'keyword' },
127129
updatedAt: { type: 'date' },

src/models/ResourceBooking.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ module.exports = (sequelize) => {
7777
type: Sequelize.STRING(255),
7878
allowNull: false
7979
},
80+
billingAccountId: {
81+
field: 'billing_account_id',
82+
type: Sequelize.BIGINT
83+
},
8084
createdBy: {
8185
field: 'created_by',
8286
type: Sequelize.UUID,

src/models/WorkPeriodPayment.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ module.exports = (sequelize) => {
5757
),
5858
allowNull: false
5959
},
60+
billingAccountId: {
61+
field: 'billing_account_id',
62+
type: Sequelize.BIGINT
63+
},
6064
createdBy: {
6165
field: 'created_by',
6266
type: Sequelize.UUID,

src/services/ResourceBookingService.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,8 @@ createResourceBooking.schema = Joi.object().keys({
174174
}),
175175
memberRate: Joi.number().allow(null),
176176
customerRate: Joi.number().allow(null),
177-
rateType: Joi.rateType().required()
177+
rateType: Joi.rateType().required(),
178+
billingAccountId: Joi.number().allow(null)
178179
}).required()
179180
}).required()
180181

@@ -230,7 +231,8 @@ partiallyUpdateResourceBooking.schema = Joi.object().keys({
230231
}),
231232
memberRate: Joi.number().allow(null),
232233
customerRate: Joi.number().allow(null),
233-
rateType: Joi.rateType()
234+
rateType: Joi.rateType(),
235+
billingAccountId: Joi.number().allow(null)
234236
}).required()
235237
}).required()
236238

@@ -268,7 +270,8 @@ fullyUpdateResourceBooking.schema = Joi.object().keys({
268270
memberRate: Joi.number().allow(null).default(null),
269271
customerRate: Joi.number().allow(null).default(null),
270272
rateType: Joi.rateType().required(),
271-
status: Joi.resourceBookingStatus().required()
273+
status: Joi.resourceBookingStatus().required(),
274+
billingAccountId: Joi.number().allow(null).default(null)
272275
}).required()
273276
}).required()
274277

src/services/WorkPeriodPaymentService.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,14 +97,24 @@ async function createWorkPeriodPayment (currentUser, workPeriodPayment, options
9797
// check permission
9898
await _checkUserPermissionForCRUWorkPeriodPayment(currentUser)
9999

100-
const { projectId, userHandle, endDate } = await helper.ensureWorkPeriodById(workPeriodPayment.workPeriodId) // ensure work period exists
100+
const { projectId, userHandle, endDate, resourceBookingId } = await helper.ensureWorkPeriodById(workPeriodPayment.workPeriodId) // ensure work period exists
101+
102+
// get billingAccountId from corresponding resource booking
103+
const correspondingResourceBooking = await helper.ensureResourceBookingById(resourceBookingId)
104+
if (!correspondingResourceBooking.billingAccountId) {
105+
throw new errors.ConflictError(`id: ${resourceBookingId} "ResourceBooking" Billing account is not assigned to the resource booking`)
106+
}
107+
workPeriodPayment.billingAccountId = correspondingResourceBooking.billingAccountId
108+
101109
const paymentChallenge = options.paymentProcessingSwitch === constants.PaymentProcessingSwitch.ON ? (await PaymentService.createPayment({
102110
projectId,
103111
userHandle,
104112
amount: workPeriodPayment.amount,
105113
name: `TaaS Payment - ${userHandle} - Week Ending ${moment(endDate).format('D/M/YYYY')}`,
106-
description: `TaaS Payment - ${userHandle} - Week Ending ${moment(endDate).format('D/M/YYYY')}`
114+
description: `TaaS Payment - ${userHandle} - Week Ending ${moment(endDate).format('D/M/YYYY')}`,
115+
billingAccountId: correspondingResourceBooking.billingAccountId
107116
})) : ({ id: '00000000-0000-0000-0000-000000000000' })
117+
108118
workPeriodPayment.id = uuid.v4()
109119
workPeriodPayment.challengeId = paymentChallenge.id
110120
workPeriodPayment.createdBy = await helper.getUserId(currentUser.userId)

test/unit/WorkPeriodPaymentService.test.js

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,61 @@ describe('workPeriod service test', () => {
2020
})
2121

2222
describe('create work period test', () => {
23-
describe('when PAYMENT_PROCESSING_SWITCH is ON/OFF', async () => {
24-
let stubCreatePaymentService
23+
let stubGetUserId
24+
let stubEnsureWorkPeriodById
25+
let stubEnsureResourceBookingById
26+
let stubCreateWorkPeriodPayment
27+
let stubCreatePayment
28+
29+
beforeEach(async () => {
30+
stubGetUserId = sinon.stub(helper, 'getUserId').callsFake(async () => testData.workPeriodPayment01.getUserIdResponse)
31+
stubEnsureWorkPeriodById = sinon.stub(helper, 'ensureWorkPeriodById').callsFake(async () => testData.workPeriodPayment01.ensureWorkPeriodByIdResponse)
32+
stubEnsureResourceBookingById = sinon.stub(helper, 'ensureResourceBookingById').callsFake(async () => testData.workPeriodPayment01.ensureResourceBookingByIdResponse)
33+
stubCreateWorkPeriodPayment = sinon.stub(models.WorkPeriodPayment, 'create').callsFake(() => testData.workPeriodPayment01.response)
34+
stubCreatePayment = sinon.stub(paymentService, 'createPayment').callsFake(async () => testData.workPeriodPayment01.createPaymentResponse)
35+
})
2536

26-
beforeEach(async () => {
27-
sinon.stub(helper, 'ensureWorkPeriodById').callsFake(async () => testData.workPeriodPayment01.ensureWorkPeriodByIdResponse)
28-
sinon.stub(helper, 'getUserId').callsFake(async () => {})
29-
sinon.stub(models.WorkPeriodPayment, 'create').callsFake(() => testData.workPeriodPayment01.response)
30-
stubCreatePaymentService = sinon.stub(paymentService, 'createPayment').callsFake(async () => testData.workPeriodPayment01.createPaymentResponse)
37+
it('create work period success', async () => {
38+
const response = await service.createWorkPeriodPayment(testData.currentUser, testData.workPeriodPayment01.request, { paymentProcessingSwitch: 'ON' })
39+
expect(stubGetUserId.calledOnce).to.be.true
40+
expect(stubEnsureWorkPeriodById.calledOnce).to.be.true
41+
expect(stubEnsureResourceBookingById.calledOnce).to.be.true
42+
expect(stubCreatePayment.calledOnce).to.be.true
43+
expect(stubCreateWorkPeriodPayment.calledOnce).to.be.true
44+
expect(response).to.eql(testData.workPeriodPayment01.response.dataValues)
45+
})
46+
47+
it('create work period success - billingAccountId is set', async () => {
48+
await service.createWorkPeriodPayment(testData.currentUser, testData.workPeriodPayment01.request, { paymentProcessingSwitch: 'ON' })
49+
expect(stubCreatePayment.calledOnce).to.be.true
50+
expect(stubCreatePayment.args[0][0]).to.include({
51+
billingAccountId: testData.workPeriodPayment01.ensureResourceBookingByIdResponse.billingAccountId
52+
})
53+
expect(stubCreateWorkPeriodPayment.calledOnce).to.be.true
54+
expect(stubCreateWorkPeriodPayment.args[0][0]).to.include({
55+
billingAccountId: testData.workPeriodPayment01.ensureResourceBookingByIdResponse.billingAccountId
3156
})
57+
})
58+
59+
it('fail to create work period if corresponding resource booking does not have bill account', async () => {
60+
stubEnsureResourceBookingById.restore()
61+
sinon.stub(helper, 'ensureResourceBookingById').callsFake(async () => testData.workPeriodPayment01.ensureResourceBookingByIdResponse02)
3262

63+
try {
64+
await service.createWorkPeriodPayment(testData.currentUser, testData.workPeriodPayment01.request)
65+
} catch (err) {
66+
expect(err.message).to.include('"ResourceBooking" Billing account is not assigned to the resource booking')
67+
}
68+
})
69+
70+
describe('when PAYMENT_PROCESSING_SWITCH is ON/OFF', async () => {
3371
it('do not create payment if PAYMENT_PROCESSING_SWITCH is OFF', async () => {
3472
await service.createWorkPeriodPayment(testData.currentUser, testData.workPeriodPayment01.request, { paymentProcessingSwitch: 'OFF' })
35-
expect(stubCreatePaymentService.calledOnce).to.be.false
73+
expect(stubCreatePayment.calledOnce).to.be.false
3674
})
3775
it('create payment if PAYMENT_PROCESSING_SWITCH is ON', async () => {
3876
await service.createWorkPeriodPayment(testData.currentUser, testData.workPeriodPayment01.request, { paymentProcessingSwitch: 'ON' })
39-
expect(stubCreatePaymentService.calledOnce).to.be.true
77+
expect(stubCreatePayment.calledOnce).to.be.true
4078
})
4179
})
4280
})

test/unit/common/testData.js

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -504,21 +504,28 @@ const workPeriodPayment01 = {
504504
status: 'completed'
505505
},
506506
response: {
507-
workPeriodId: '467b4df7-ced4-41b9-9710-b83808cddaf4',
508-
amount: 600,
509-
status: 'completed',
510-
id: '01971e6f-0f09-4a2a-bc2e-2adac0f00622',
511-
challengeId: '00000000-0000-0000-0000-000000000000',
512-
createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c',
513-
updatedAt: '2021-04-21T12:58:07.535Z',
514-
createdAt: '2021-04-21T12:58:07.535Z',
515-
updatedBy: null
507+
dataValues: {
508+
workPeriodId: '467b4df7-ced4-41b9-9710-b83808cddaf4',
509+
amount: 600,
510+
status: 'completed',
511+
id: '01971e6f-0f09-4a2a-bc2e-2adac0f00622',
512+
challengeId: '00000000-0000-0000-0000-000000000000',
513+
createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c',
514+
updatedAt: '2021-04-21T12:58:07.535Z',
515+
createdAt: '2021-04-21T12:58:07.535Z',
516+
updatedBy: null
517+
}
516518
},
519+
getUserIdResponse: '79a39efd-91af-494a-b0f6-62310495effd',
517520
ensureWorkPeriodByIdResponse: {
518521
projectId: 111,
519522
userHandle: 'pshah_manager',
520523
endDate: '2021-03-13'
521524
},
525+
ensureResourceBookingByIdResponse: {
526+
billingAccountId: 68800079
527+
},
528+
ensureResourceBookingByIdResponse02: {},
522529
createPaymentResponse: {
523530
id: 'c65f0cbf-b197-423d-91cc-db6e3bad9075'
524531
}

0 commit comments

Comments
 (0)