Skip to content

Use billing account for payments #217

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 40 additions & 40 deletions docs/Topcoder-bookings-api.postman_collection.json

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3162,6 +3162,10 @@ components:
type: string
enum: ["hourly", "daily", "weekly", "monthly"]
description: "The rate type of the job."
billingAccountId:
type: integer
example: 80000071
description: 'the billing account id for payments'
createdAt:
type: string
format: date-time
Expand Down Expand Up @@ -3226,6 +3230,10 @@ components:
type: string
enum: ["hourly", "daily", "weekly", "monthly"]
description: "The rate type of the job."
billingAccountId:
type: integer
example: 80000071
description: 'the billing account id for payments'
ResourceBookingPatchRequestBody:
properties:
status:
Expand Down Expand Up @@ -3255,6 +3263,10 @@ components:
type: string
enum: ["hourly", "daily", "weekly", "monthly"]
description: "The rate type of the job."
billingAccountId:
type: integer
example: 80000071
description: 'the billing account id for payments'
WorkPeriod:
required:
- id
Expand Down Expand Up @@ -3435,6 +3447,10 @@ components:
type: string
enum: ["completed", "cancelled"]
description: "The payment status."
billingAccountId:
type: integer
example: 80000071
description: 'the billing account id for payments'
createdAt:
type: string
format: date-time
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const config = require('config')

/*
* Add billingAccountId field to the ResourceBooking and WorkPeriodPayment models.
*/

module.exports = {
up: async (queryInterface, Sequelize) => {
const transaction = await queryInterface.sequelize.transaction()
try {
for (const tableName of ['resource_bookings', 'work_period_payments']) {
await queryInterface.addColumn({ tableName, schema: config.DB_SCHEMA_NAME }, 'billing_account_id',
{ type: Sequelize.BIGINT },
{ transaction })
}
await transaction.commit()
} catch (err) {
await transaction.rollback()
throw err
}
},
down: async (queryInterface, Sequelize) => {
for (const tableName of ['resource_bookings', 'work_period_payments']) {
await queryInterface.removeColumn({ tableName, schema: config.DB_SCHEMA_NAME }, 'billing_account_id')
}
}
}
1 change: 1 addition & 0 deletions src/common/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,6 @@ module.exports = {
UnauthorizedError: createError('UnauthorizedError', 401),
ForbiddenError: createError('ForbiddenError', 403),
NotFoundError: createError('NotFoundError', 404),
ConflictError: createError('ConflictError', 409),
InternalServerError: createError('InternalServerError', 500)
}
2 changes: 2 additions & 0 deletions src/common/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_RESOURCE_BOOKING')] = {
memberRate: { type: 'float' },
customerRate: { type: 'float' },
rateType: { type: 'keyword' },
billingAccountId: { type: 'integer' },
createdAt: { type: 'date' },
createdBy: { type: 'keyword' },
updatedAt: { type: 'date' },
Expand All @@ -122,6 +123,7 @@ esIndexPropertyMapping[config.get('esConfig.ES_INDEX_WORK_PERIOD')] = {
challengeId: { type: 'keyword' },
amount: { type: 'float' },
status: { type: 'keyword' },
billingAccountId: { type: 'integer' },
createdAt: { type: 'date' },
createdBy: { type: 'keyword' },
updatedAt: { type: 'date' },
Expand Down
4 changes: 4 additions & 0 deletions src/models/ResourceBooking.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ module.exports = (sequelize) => {
type: Sequelize.STRING(255),
allowNull: false
},
billingAccountId: {
field: 'billing_account_id',
type: Sequelize.BIGINT
},
createdBy: {
field: 'created_by',
type: Sequelize.UUID,
Expand Down
4 changes: 4 additions & 0 deletions src/models/WorkPeriodPayment.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ module.exports = (sequelize) => {
),
allowNull: false
},
billingAccountId: {
field: 'billing_account_id',
type: Sequelize.BIGINT
},
createdBy: {
field: 'created_by',
type: Sequelize.UUID,
Expand Down
9 changes: 6 additions & 3 deletions src/services/ResourceBookingService.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,8 @@ createResourceBooking.schema = Joi.object().keys({
}),
memberRate: Joi.number().allow(null),
customerRate: Joi.number().allow(null),
rateType: Joi.rateType().required()
rateType: Joi.rateType().required(),
billingAccountId: Joi.number().allow(null)
}).required()
}).required()

Expand Down Expand Up @@ -230,7 +231,8 @@ partiallyUpdateResourceBooking.schema = Joi.object().keys({
}),
memberRate: Joi.number().allow(null),
customerRate: Joi.number().allow(null),
rateType: Joi.rateType()
rateType: Joi.rateType(),
billingAccountId: Joi.number().allow(null)
}).required()
}).required()

Expand Down Expand Up @@ -268,7 +270,8 @@ fullyUpdateResourceBooking.schema = Joi.object().keys({
memberRate: Joi.number().allow(null).default(null),
customerRate: Joi.number().allow(null).default(null),
rateType: Joi.rateType().required(),
status: Joi.resourceBookingStatus().required()
status: Joi.resourceBookingStatus().required(),
billingAccountId: Joi.number().allow(null).default(null)
}).required()
}).required()

Expand Down
14 changes: 12 additions & 2 deletions src/services/WorkPeriodPaymentService.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,24 @@ async function createWorkPeriodPayment (currentUser, workPeriodPayment, options
// check permission
await _checkUserPermissionForCRUWorkPeriodPayment(currentUser)

const { projectId, userHandle, endDate } = await helper.ensureWorkPeriodById(workPeriodPayment.workPeriodId) // ensure work period exists
const { projectId, userHandle, endDate, resourceBookingId } = await helper.ensureWorkPeriodById(workPeriodPayment.workPeriodId) // ensure work period exists

// get billingAccountId from corresponding resource booking
const correspondingResourceBooking = await helper.ensureResourceBookingById(resourceBookingId)
if (!correspondingResourceBooking.billingAccountId) {
throw new errors.ConflictError(`id: ${resourceBookingId} "ResourceBooking" Billing account is not assigned to the resource booking`)
}
workPeriodPayment.billingAccountId = correspondingResourceBooking.billingAccountId

const paymentChallenge = options.paymentProcessingSwitch === constants.PaymentProcessingSwitch.ON ? (await PaymentService.createPayment({
projectId,
userHandle,
amount: workPeriodPayment.amount,
name: `TaaS Payment - ${userHandle} - Week Ending ${moment(endDate).format('D/M/YYYY')}`,
description: `TaaS Payment - ${userHandle} - Week Ending ${moment(endDate).format('D/M/YYYY')}`
description: `TaaS Payment - ${userHandle} - Week Ending ${moment(endDate).format('D/M/YYYY')}`,
billingAccountId: correspondingResourceBooking.billingAccountId
})) : ({ id: '00000000-0000-0000-0000-000000000000' })

workPeriodPayment.id = uuid.v4()
workPeriodPayment.challengeId = paymentChallenge.id
workPeriodPayment.createdBy = await helper.getUserId(currentUser.userId)
Expand Down
56 changes: 47 additions & 9 deletions test/unit/WorkPeriodPaymentService.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,61 @@ describe('workPeriod service test', () => {
})

describe('create work period test', () => {
describe('when PAYMENT_PROCESSING_SWITCH is ON/OFF', async () => {
let stubCreatePaymentService
let stubGetUserId
let stubEnsureWorkPeriodById
let stubEnsureResourceBookingById
let stubCreateWorkPeriodPayment
let stubCreatePayment

beforeEach(async () => {
stubGetUserId = sinon.stub(helper, 'getUserId').callsFake(async () => testData.workPeriodPayment01.getUserIdResponse)
stubEnsureWorkPeriodById = sinon.stub(helper, 'ensureWorkPeriodById').callsFake(async () => testData.workPeriodPayment01.ensureWorkPeriodByIdResponse)
stubEnsureResourceBookingById = sinon.stub(helper, 'ensureResourceBookingById').callsFake(async () => testData.workPeriodPayment01.ensureResourceBookingByIdResponse)
stubCreateWorkPeriodPayment = sinon.stub(models.WorkPeriodPayment, 'create').callsFake(() => testData.workPeriodPayment01.response)
stubCreatePayment = sinon.stub(paymentService, 'createPayment').callsFake(async () => testData.workPeriodPayment01.createPaymentResponse)
})

beforeEach(async () => {
sinon.stub(helper, 'ensureWorkPeriodById').callsFake(async () => testData.workPeriodPayment01.ensureWorkPeriodByIdResponse)
sinon.stub(helper, 'getUserId').callsFake(async () => {})
sinon.stub(models.WorkPeriodPayment, 'create').callsFake(() => testData.workPeriodPayment01.response)
stubCreatePaymentService = sinon.stub(paymentService, 'createPayment').callsFake(async () => testData.workPeriodPayment01.createPaymentResponse)
it('create work period success', async () => {
const response = await service.createWorkPeriodPayment(testData.currentUser, testData.workPeriodPayment01.request, { paymentProcessingSwitch: 'ON' })
expect(stubGetUserId.calledOnce).to.be.true
expect(stubEnsureWorkPeriodById.calledOnce).to.be.true
expect(stubEnsureResourceBookingById.calledOnce).to.be.true
expect(stubCreatePayment.calledOnce).to.be.true
expect(stubCreateWorkPeriodPayment.calledOnce).to.be.true
expect(response).to.eql(testData.workPeriodPayment01.response.dataValues)
})

it('create work period success - billingAccountId is set', async () => {
await service.createWorkPeriodPayment(testData.currentUser, testData.workPeriodPayment01.request, { paymentProcessingSwitch: 'ON' })
expect(stubCreatePayment.calledOnce).to.be.true
expect(stubCreatePayment.args[0][0]).to.include({
billingAccountId: testData.workPeriodPayment01.ensureResourceBookingByIdResponse.billingAccountId
})
expect(stubCreateWorkPeriodPayment.calledOnce).to.be.true
expect(stubCreateWorkPeriodPayment.args[0][0]).to.include({
billingAccountId: testData.workPeriodPayment01.ensureResourceBookingByIdResponse.billingAccountId
})
})

it('fail to create work period if corresponding resource booking does not have bill account', async () => {
stubEnsureResourceBookingById.restore()
sinon.stub(helper, 'ensureResourceBookingById').callsFake(async () => testData.workPeriodPayment01.ensureResourceBookingByIdResponse02)

try {
await service.createWorkPeriodPayment(testData.currentUser, testData.workPeriodPayment01.request)
} catch (err) {
expect(err.message).to.include('"ResourceBooking" Billing account is not assigned to the resource booking')
}
})

describe('when PAYMENT_PROCESSING_SWITCH is ON/OFF', async () => {
it('do not create payment if PAYMENT_PROCESSING_SWITCH is OFF', async () => {
await service.createWorkPeriodPayment(testData.currentUser, testData.workPeriodPayment01.request, { paymentProcessingSwitch: 'OFF' })
expect(stubCreatePaymentService.calledOnce).to.be.false
expect(stubCreatePayment.calledOnce).to.be.false
})
it('create payment if PAYMENT_PROCESSING_SWITCH is ON', async () => {
await service.createWorkPeriodPayment(testData.currentUser, testData.workPeriodPayment01.request, { paymentProcessingSwitch: 'ON' })
expect(stubCreatePaymentService.calledOnce).to.be.true
expect(stubCreatePayment.calledOnce).to.be.true
})
})
})
Expand Down
25 changes: 16 additions & 9 deletions test/unit/common/testData.js
Original file line number Diff line number Diff line change
Expand Up @@ -504,21 +504,28 @@ const workPeriodPayment01 = {
status: 'completed'
},
response: {
workPeriodId: '467b4df7-ced4-41b9-9710-b83808cddaf4',
amount: 600,
status: 'completed',
id: '01971e6f-0f09-4a2a-bc2e-2adac0f00622',
challengeId: '00000000-0000-0000-0000-000000000000',
createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c',
updatedAt: '2021-04-21T12:58:07.535Z',
createdAt: '2021-04-21T12:58:07.535Z',
updatedBy: null
dataValues: {
workPeriodId: '467b4df7-ced4-41b9-9710-b83808cddaf4',
amount: 600,
status: 'completed',
id: '01971e6f-0f09-4a2a-bc2e-2adac0f00622',
challengeId: '00000000-0000-0000-0000-000000000000',
createdBy: '57646ff9-1cd3-4d3c-88ba-eb09a395366c',
updatedAt: '2021-04-21T12:58:07.535Z',
createdAt: '2021-04-21T12:58:07.535Z',
updatedBy: null
}
},
getUserIdResponse: '79a39efd-91af-494a-b0f6-62310495effd',
ensureWorkPeriodByIdResponse: {
projectId: 111,
userHandle: 'pshah_manager',
endDate: '2021-03-13'
},
ensureResourceBookingByIdResponse: {
billingAccountId: 68800079
},
ensureResourceBookingByIdResponse02: {},
createPaymentResponse: {
id: 'c65f0cbf-b197-423d-91cc-db6e3bad9075'
}
Expand Down