diff --git a/docs/swagger.yaml b/docs/swagger.yaml index c3706ee..c2d9dcb 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -189,6 +189,15 @@ paths: required: false type: integer minimum: 1 + - name: projectIds + in: query + description: Filter by multiple v5 project ids (array). Use repeated query params, e.g. projectIds[]=1&projectIds[]=2. + required: false + type: array + items: + type: integer + minimum: 1 + collectionFormat: brackets - name: forumId in: query description: Filter by forum id, exact match. diff --git a/src/services/ChallengeService.js b/src/services/ChallengeService.js index a94c97b..59347ad 100644 --- a/src/services/ChallengeService.js +++ b/src/services/ChallengeService.js @@ -203,66 +203,6 @@ function applyChallengeApprovalFlowBypass(target, billingAccountId) { return true; } -/** - * Applies the temporary create-time approval hotfix to a challenge payload. - * - * Challenges created in NEW or DRAFT status are forced to approved while the - * budget approval flow is being investigated. A missing create status is - * treated as NEW because createChallenge defaults it later in the workflow. - * - * @param {Object} challenge Challenge create payload to mutate. - * @returns {boolean} `true` when approval fields were forced to approved. - */ -function applyCreateChallengeApprovalStatusHotfix(challenge) { - const challengeStatus = normalizeStatusSortValue(challenge.status || ChallengeStatusEnum.NEW); - - if ( - challengeStatus !== ChallengeStatusEnum.NEW && - challengeStatus !== ChallengeStatusEnum.DRAFT - ) { - return false; - } - - challenge.approvalStatus = CHALLENGE_APPROVAL_STATUS.APPROVED; - challenge.approvalRejectionReason = null; - challenge.approvalApprovedBy = null; - - return true; -} - -/** - * Applies the temporary NEW-to-DRAFT approval preservation hotfix to an update payload. - * - * Challenges auto-approved during creation must keep that approved state when - * saved from NEW to DRAFT, even if the same PATCH includes prize data. - * - * @param {Object} challenge Existing challenge response payload. - * @param {Object} data Sanitized challenge update payload to mutate. - * @param {string|null|undefined} requestedApprovalStatus Valid approval status from the update payload. - * @returns {boolean} `true` when approval fields were forced to remain approved. - */ -function applyNewDraftApprovalStatusPreservationHotfix(challenge, data, requestedApprovalStatus) { - const currentStatus = normalizeStatusSortValue(challenge.status); - const targetStatus = normalizeStatusSortValue(data.status || challenge.status); - const currentApprovalStatus = normalizeApprovalStatus(challenge.approvalStatus); - - if ( - currentStatus !== ChallengeStatusEnum.NEW || - targetStatus !== ChallengeStatusEnum.DRAFT || - currentApprovalStatus !== CHALLENGE_APPROVAL_STATUS.APPROVED || - (requestedApprovalStatus != null && - requestedApprovalStatus !== CHALLENGE_APPROVAL_STATUS.APPROVED) - ) { - return false; - } - - data.approvalStatus = CHALLENGE_APPROVAL_STATUS.APPROVED; - data.approvalRejectionReason = null; - delete data.approvalApprovedBy; - - return true; -} - /** * Determines whether challenge activation must wait for budget approval. * @@ -1403,6 +1343,13 @@ async function searchChallenges(currentUser, criteria) { } }); + // handle projectIds (array of project IDs, applied as IN filter) + if (Array.isArray(criteria.projectIds) && criteria.projectIds.length > 0) { + prismaFilter.where.AND.push({ + projectId: { in: criteria.projectIds }, + }); + } + // handle status if (!_.isNil(criteria.status)) { prismaFilter.where.AND.push({ @@ -1410,6 +1357,13 @@ async function searchChallenges(currentUser, criteria) { }); } + // handle approvalStatus + if (!_.isNil(criteria.approvalStatus)) { + prismaFilter.where.AND.push({ + approvalStatus: criteria.approvalStatus.toUpperCase(), + }); + } + _.forEach(_.keys(criteria), (key) => { if (_.toString(key).indexOf("meta.") > -1) { // Parse and use metadata key @@ -2124,11 +2078,15 @@ searchChallenges.schema = { tags: Joi.array().items(Joi.string()), includeAllTags: Joi.boolean().default(true), projectId: Joi.number().integer().positive(), + projectIds: Joi.array().items(Joi.number().integer().positive()), forumId: Joi.number().integer(), legacyId: Joi.number().integer().positive(), status: Joi.string() .valid(..._.values(ChallengeStatusEnum)) .insensitive(), + approvalStatus: Joi.string() + .valid(..._.values(CHALLENGE_APPROVAL_STATUS)) + .insensitive(), group: Joi.string(), startDateStart: Joi.date(), startDateEnd: Joi.date(), @@ -2170,7 +2128,6 @@ searchChallenges.schema = { /** * Create challenge. - * Temporary hotfix: NEW and DRAFT challenge creations are auto-approved. * Challenges billed to configured Topgear accounts skip manual budget approval and are auto-approved. * @param {Object} currentUser the user who perform operation * @param {Object} challenge the challenge to created @@ -2280,15 +2237,11 @@ async function createChallenge(currentUser, challenge, userToken) { challenge.status = ChallengeStatusEnum.NEW; } - let skipsChallengeApprovalFlow = applyCreateChallengeApprovalStatusHotfix(challenge); - - if (!skipsChallengeApprovalFlow) { - const approvalBillingAccountId = getApprovalFlowBillingAccountId(challenge); - skipsChallengeApprovalFlow = applyChallengeApprovalFlowBypass( - challenge, - approvalBillingAccountId, - ); - } + const approvalBillingAccountId = getApprovalFlowBillingAccountId(challenge); + const skipsChallengeApprovalFlow = applyChallengeApprovalFlowBypass( + challenge, + approvalBillingAccountId, + ); if (!skipsChallengeApprovalFlow) { const requestedApprovalStatus = normalizeApprovalStatus(challenge.approvalStatus); @@ -3412,16 +3365,9 @@ async function updateChallenge(currentUser, challengeId, data, options = {}) { data.approvalApprovedBy = null; } - const preservesNewDraftApprovalStatus = applyNewDraftApprovalStatusPreservationHotfix( - challenge, - data, - requestedApprovalStatus, - ); - if ( prizeSetsUpdated && challenge.status !== ChallengeStatusEnum.ACTIVE && - !preservesNewDraftApprovalStatus && (requestedApprovalStatus == null || !canApproveChallengeBudget) ) { data.approvalStatus = CHALLENGE_APPROVAL_STATUS.PENDING_APPROVAL; @@ -5347,8 +5293,6 @@ async function indexChallengeAndPostToKafka(updatedChallenge, track, type) { module.exports = { __testables: { - applyCreateChallengeApprovalStatusHotfix, - applyNewDraftApprovalStatusPreservationHotfix, shouldBlockChallengeLaunchForApproval, shouldSkipChallengeApprovalFlow, syncChallengeBillingAccountLock, diff --git a/test/unit/ChallengeService.test.js b/test/unit/ChallengeService.test.js index f7002cf..f3a899c 100644 --- a/test/unit/ChallengeService.test.js +++ b/test/unit/ChallengeService.test.js @@ -254,6 +254,7 @@ describe("challenge service unit tests", () => { should.equal(result.legacyId, testChallengeData.legacyId); should.equal(result.forumId, testChallengeData.forumId); should.equal(result.status, testChallengeData.status); + should.equal(result.approvalStatus, "PENDING_APPROVAL"); should.equal(result.funChallenge, testChallengeData.funChallenge); should.equal(result.createdBy, "testuser"); should.exist(result.startDate); @@ -1190,6 +1191,16 @@ describe("challenge service unit tests", () => { should.equal(result.result[0].name, data.challenge.name); }); + it("search challenges by approvalStatus case-insensitively", async () => { + const result = await service.searchChallenges( + { isMachine: true }, + { approvalStatus: "approved" }, + ); + + should.equal(result.total > 0, true); + should.equal(result.result.every((challenge) => challenge.approvalStatus === "APPROVED"), true); + }); + it("search challenges successfully 3", async () => { const res = await service.searchChallenges( { isMachine: true }, @@ -1403,6 +1414,16 @@ describe("challenge service unit tests", () => { } throw new Error("should not reach here"); }); + + it("search challenges - invalid approvalStatus", async () => { + try { + await service.searchChallenges({ isMachine: true }, { approvalStatus: "INVALID" }); + } catch (e) { + should.equal(e.message.includes("approvalStatus") && e.message.includes("must be one of"), true); + return; + } + throw new Error("should not reach here"); + }); }); describe("update challenge tests", () => { @@ -1759,7 +1780,7 @@ describe("challenge service unit tests", () => { config.M2M_FULL_ACCESS_TOKEN, ); createdChallengeId = created.id; - should.equal(created.approvalStatus, "APPROVED"); + should.equal(created.approvalStatus, "PENDING_APPROVAL"); should.equal(billingLockRequests.length, 0); const draftPrizeSets = _.cloneDeep(challengeData.prizeSets); @@ -1769,13 +1790,12 @@ describe("challenge service unit tests", () => { { isMachine: true, sub: "sub-billing-lock-update", userId: 22838965 }, created.id, { - approvalStatus: "APPROVED", prizeSets: draftPrizeSets, status: ChallengeStatusEnum.DRAFT, }, ); - should.equal(draft.approvalStatus, "APPROVED"); + should.equal(draft.approvalStatus, "PENDING_APPROVAL"); should.equal(draft.billing.billingAccountId, "80001012"); should.equal(billingLockRequests.length, 1); billingLockRequests[0].should.deep.equal({ diff --git a/test/unit/challenge-activation-billing.test.js b/test/unit/challenge-activation-billing.test.js index 4e9c25e..4f6661c 100644 --- a/test/unit/challenge-activation-billing.test.js +++ b/test/unit/challenge-activation-billing.test.js @@ -12,10 +12,6 @@ const { ChallengeStatusEnum } = require("../../src/common/prisma"); const should = chai.should(); describe("challenge activation billing validation unit tests", () => { - const applyCreateChallengeApprovalStatusHotfix = - service.__testables.applyCreateChallengeApprovalStatusHotfix; - const applyNewDraftApprovalStatusPreservationHotfix = - service.__testables.applyNewDraftApprovalStatusPreservationHotfix; const validateChallengeActivationBillingAccount = service.__testables.validateChallengeActivationBillingAccount; const shouldBlockChallengeLaunchForApproval = @@ -169,78 +165,6 @@ describe("challenge activation billing validation unit tests", () => { should.equal(shouldBlockChallengeLaunchForApproval("APPROVED", "80001061"), false); }); - it("auto-approves NEW and DRAFT challenge creation payloads", () => { - const defaultNewChallenge = {}; - const draftChallenge = { - status: ChallengeStatusEnum.DRAFT, - approvalStatus: "REJECTED", - approvalRejectionReason: "too expensive", - approvalApprovedBy: "approver", - }; - const approvedChallenge = { - status: ChallengeStatusEnum.APPROVED, - }; - - should.equal(applyCreateChallengeApprovalStatusHotfix(defaultNewChallenge), true); - should.equal(defaultNewChallenge.approvalStatus, "APPROVED"); - should.equal(defaultNewChallenge.approvalRejectionReason, null); - should.equal(defaultNewChallenge.approvalApprovedBy, null); - - should.equal(applyCreateChallengeApprovalStatusHotfix(draftChallenge), true); - should.equal(draftChallenge.approvalStatus, "APPROVED"); - should.equal(draftChallenge.approvalRejectionReason, null); - should.equal(draftChallenge.approvalApprovedBy, null); - - should.equal(applyCreateChallengeApprovalStatusHotfix(approvedChallenge), false); - should.equal(approvedChallenge.approvalStatus, undefined); - }); - - it("keeps approved status when an approved NEW challenge is saved as DRAFT", () => { - const existingChallenge = { - status: ChallengeStatusEnum.NEW, - approvalStatus: "APPROVED", - approvalApprovedBy: "existing-approver", - }; - const updatePayload = { - status: ChallengeStatusEnum.DRAFT, - approvalApprovedBy: "incoming-approver", - }; - const pendingChallenge = { - status: ChallengeStatusEnum.NEW, - approvalStatus: "PENDING_APPROVAL", - }; - const pendingUpdatePayload = { - status: ChallengeStatusEnum.DRAFT, - }; - const rejectedUpdatePayload = { - status: ChallengeStatusEnum.DRAFT, - }; - - should.equal( - applyNewDraftApprovalStatusPreservationHotfix(existingChallenge, updatePayload), - true, - ); - should.equal(updatePayload.approvalStatus, "APPROVED"); - should.equal(updatePayload.approvalRejectionReason, null); - should.equal(updatePayload.approvalApprovedBy, undefined); - - should.equal( - applyNewDraftApprovalStatusPreservationHotfix(pendingChallenge, pendingUpdatePayload), - false, - ); - should.equal(pendingUpdatePayload.approvalStatus, undefined); - - should.equal( - applyNewDraftApprovalStatusPreservationHotfix( - existingChallenge, - rejectedUpdatePayload, - "REJECTED", - ), - false, - ); - should.equal(rejectedUpdatePayload.approvalStatus, undefined); - }); - it("skips budget lock funds validation for ignored billing accounts", async () => { config.IGNORED_CHALLENGE_ACTIVATION_BILLING_ACCOUNT_IDS = ["80000062"]; let lockCalled = false;