Skip to content

Commit f27d366

Browse files
committed
PM-4957: Trigger finance on challenge cancellation
What was broken Cancelled challenges in Appeals Response could be saved in challenge-api-v6 without ever calling tc-finance-api, so reviewer payments were not generated and billing-account ledger rows were not updated. Root cause The challenge update flow only requested finance payment generation for task challenges transitioning to COMPLETED. Cancelled status transitions reused phase-closing logic but had no matching finance trigger. What was changed Added a shared cancelled-status helper in ChallengeService and call finance after a challenge first transitions into any cancelled terminal status. The call runs after the database transaction commits, matching the existing completed-task payment generation pattern. Any added/updated tests Added a ChallengeService unit test that stubs the finance helper and verifies a challenge transitioning to CANCELLED_CLIENT_REQUEST requests payment generation.
1 parent 8fca740 commit f27d366

2 files changed

Lines changed: 70 additions & 16 deletions

File tree

src/services/ChallengeService.js

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,27 @@ const allowedSortByValues = _.uniq([
5858
...Object.keys(sortByAliases),
5959
]);
6060

61+
const CANCELLED_CHALLENGE_STATUSES = new Set([
62+
ChallengeStatusEnum.CANCELLED,
63+
ChallengeStatusEnum.CANCELLED_REQUIREMENTS_INFEASIBLE,
64+
ChallengeStatusEnum.CANCELLED_PAYMENT_FAILED,
65+
ChallengeStatusEnum.CANCELLED_FAILED_REVIEW,
66+
ChallengeStatusEnum.CANCELLED_FAILED_SCREENING,
67+
ChallengeStatusEnum.CANCELLED_ZERO_SUBMISSIONS,
68+
ChallengeStatusEnum.CANCELLED_WINNER_UNRESPONSIVE,
69+
ChallengeStatusEnum.CANCELLED_CLIENT_REQUEST,
70+
ChallengeStatusEnum.CANCELLED_ZERO_REGISTRATIONS,
71+
]);
72+
73+
/**
74+
* Determines whether a challenge status is one of the terminal cancelled states.
75+
* @param {String} status challenge status from the update payload or stored challenge
76+
* @returns {Boolean} true when the status represents a cancelled challenge
77+
*/
78+
function isCancelledChallengeStatus(status) {
79+
return CANCELLED_CHALLENGE_STATUSES.has(status);
80+
}
81+
6182
function normalizeStatusSortValue(statusValue) {
6283
if (_.isNil(statusValue)) {
6384
return null;
@@ -2792,6 +2813,8 @@ function prepareTaskCompletionData(challenge, challengeResources, data) {
27922813

27932814
/**
27942815
* Update challenge.
2816+
* When a challenge transitions to completed task status or a cancelled status,
2817+
* payment generation is requested after the database update commits.
27952818
* @param {Object} currentUser the user who perform operation
27962819
* @param {String} challengeId the challenge id
27972820
* @param {Object} data the challenge data to be updated
@@ -3005,6 +3028,8 @@ async function updateChallenge(currentUser, challengeId, data, options = {}) {
30053028

30063029
let isChallengeBeingActivated = isStatusChangingToActive;
30073030
let isChallengeBeingCancelled = false;
3031+
const isStatusChangingToCancelled =
3032+
isCancelledChallengeStatus(data.status) && !isCancelledChallengeStatus(challenge.status);
30083033
if (data.status) {
30093034
if (data.status === ChallengeStatusEnum.ACTIVE) {
30103035
await validateChallengeActivationBillingAccount({
@@ -3015,22 +3040,7 @@ async function updateChallenge(currentUser, challengeId, data, options = {}) {
30153040
});
30163041
}
30173042

3018-
if (
3019-
_.includes(
3020-
[
3021-
ChallengeStatusEnum.CANCELLED,
3022-
ChallengeStatusEnum.CANCELLED_REQUIREMENTS_INFEASIBLE,
3023-
ChallengeStatusEnum.CANCELLED_PAYMENT_FAILED,
3024-
ChallengeStatusEnum.CANCELLED_FAILED_REVIEW,
3025-
ChallengeStatusEnum.CANCELLED_FAILED_SCREENING,
3026-
ChallengeStatusEnum.CANCELLED_ZERO_SUBMISSIONS,
3027-
ChallengeStatusEnum.CANCELLED_WINNER_UNRESPONSIVE,
3028-
ChallengeStatusEnum.CANCELLED_CLIENT_REQUEST,
3029-
ChallengeStatusEnum.CANCELLED_ZERO_REGISTRATIONS,
3030-
],
3031-
data.status,
3032-
)
3033-
) {
3043+
if (isCancelledChallengeStatus(data.status)) {
30343044
isChallengeBeingCancelled = true;
30353045
}
30363046

@@ -3584,6 +3594,19 @@ async function updateChallenge(currentUser, challengeId, data, options = {}) {
35843594
logger.error(`Error generating payments for Task challenge ${challengeId}: ${err.message}`);
35853595
}
35863596
}
3597+
if (isStatusChangingToCancelled) {
3598+
logger.info(`Triggering payment generation for cancelled challenge ${challengeId}`);
3599+
try {
3600+
const paymentSuccess = await helper.generateChallengePayments(challengeId);
3601+
if (!paymentSuccess) {
3602+
logger.warn(`Failed to generate payments for cancelled challenge ${challengeId}`);
3603+
}
3604+
} catch (err) {
3605+
logger.error(
3606+
`Error generating payments for cancelled challenge ${challengeId}: ${err.message}`,
3607+
);
3608+
}
3609+
}
35873610
// Re-fetch the challenge outside the transaction to ensure we publish
35883611
// only after the commit succeeds and using the committed snapshot.
35893612
if (emitEvent) {

test/unit/ChallengeService.test.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1800,6 +1800,37 @@ describe("challenge service unit tests", () => {
18001800
}
18011801
});
18021802

1803+
it("update challenge - triggers payments when a challenge is cancelled", async () => {
1804+
const originalGetChallengeResources = helper.getChallengeResources;
1805+
const originalGenerateChallengePayments = helper.generateChallengePayments;
1806+
let generatedPaymentsChallengeId;
1807+
const cancelledChallenge = await createActivationChallenge(ChallengeStatusEnum.ACTIVE);
1808+
1809+
helper.getChallengeResources = async () => [];
1810+
helper.generateChallengePayments = async (challengeId) => {
1811+
generatedPaymentsChallengeId = challengeId;
1812+
return true;
1813+
};
1814+
1815+
try {
1816+
const result = await service.updateChallenge(
1817+
{ isMachine: true, sub: "sub-cancel", userId: 22838965 },
1818+
cancelledChallenge.id,
1819+
{
1820+
status: ChallengeStatusEnum.CANCELLED_CLIENT_REQUEST,
1821+
cancelReason: "QA cancellation coverage",
1822+
},
1823+
);
1824+
1825+
should.equal(result.status, ChallengeStatusEnum.CANCELLED_CLIENT_REQUEST);
1826+
should.equal(generatedPaymentsChallengeId, cancelledChallenge.id);
1827+
} finally {
1828+
helper.getChallengeResources = originalGetChallengeResources;
1829+
helper.generateChallengePayments = originalGenerateChallengePayments;
1830+
await prisma.challenge.deleteMany({ where: { id: cancelledChallenge.id } });
1831+
}
1832+
});
1833+
18031834
describe("reviewer scorecard changes", () => {
18041835
const originalScorecardId = "sc-original";
18051836
const newScorecardId = "sc-updated";

0 commit comments

Comments
 (0)