Skip to content

Commit fd38ea9

Browse files
committed
Better handling of draft challenges and locking
1 parent e21f84a commit fd38ea9

2 files changed

Lines changed: 172 additions & 12 deletions

File tree

src/services/ChallengeService.js

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,50 @@ function isChallengeBillingLockStatus(status) {
105105
}
106106

107107
/**
108-
* Reads the currently persisted challenge prize total.
108+
* Calculates the billable USD prize-set total for a challenge.
109109
*
110110
* @param {object} challenge Challenge model or response object.
111-
* @returns {number|undefined} Total member-payment amount before markup.
111+
* @returns {number|undefined} USD prize-set member-payment amount before markup,
112+
* or `undefined` when prize sets are not loaded.
113+
*/
114+
function getChallengePrizeSetMemberPaymentAmount(challenge) {
115+
const prizeSets = _.get(challenge, "prizeSets");
116+
117+
if (!Array.isArray(prizeSets)) {
118+
return undefined;
119+
}
120+
121+
return prizeSets.reduce((total, prizeSet) => {
122+
const prizes = Array.isArray(prizeSet && prizeSet.prizes) ? prizeSet.prizes : [];
123+
124+
return (
125+
total +
126+
prizes.reduce((prizeTotal, prize) => {
127+
if (_.toString(_.get(prize, "type")).toUpperCase() !== constants.prizeTypes.USD) {
128+
return prizeTotal;
129+
}
130+
131+
const prizeValue = _.toNumber(_.get(prize, "value"));
132+
133+
return Number.isFinite(prizeValue) ? prizeTotal + prizeValue : prizeTotal;
134+
}, 0)
135+
);
136+
}, 0);
137+
}
138+
139+
/**
140+
* Reads the currently persisted challenge member-payment total.
141+
*
142+
* @param {object} challenge Challenge model or response object.
143+
* @returns {number|undefined} Total USD member-payment amount before markup.
112144
*/
113145
function getChallengeMemberPaymentAmount(challenge) {
146+
const prizeSetMemberPaymentAmount = getChallengePrizeSetMemberPaymentAmount(challenge);
147+
148+
if (!_.isNil(prizeSetMemberPaymentAmount)) {
149+
return prizeSetMemberPaymentAmount;
150+
}
151+
114152
const totalPrizes = _.get(
115153
challenge,
116154
"overview.totalPrizes",
@@ -2916,14 +2954,17 @@ async function updateChallenge(currentUser, challengeId, data, options = {}) {
29162954
})`,
29172955
);
29182956

2919-
if (billingAccountId && _.isUndefined(_.get(challenge, "billing.billingAccountId"))) {
2920-
// Ensure billingAccountId is a string or null to match Prisma schema
2921-
if (billingAccountId !== null && billingAccountId !== undefined) {
2922-
_.set(data, "billing.billingAccountId", String(billingAccountId));
2923-
} else {
2924-
_.set(data, "billing.billingAccountId", null);
2925-
}
2926-
_.set(data, "billing.markup", _.isNil(markup) ? 0 : markup);
2957+
const existingBillingAccountId = normalizeOptionalString(
2958+
_.get(challenge, "billing.billingAccountId"),
2959+
);
2960+
const projectBillingAccountId = normalizeOptionalString(billingAccountId);
2961+
if (projectBillingAccountId && !existingBillingAccountId) {
2962+
data.billing = {
2963+
..._.get(challenge, "billing", {}),
2964+
..._.get(data, "billing", {}),
2965+
billingAccountId: projectBillingAccountId,
2966+
markup: _.isNil(markup) ? 0 : markup,
2967+
};
29272968
}
29282969

29292970
// Make sure the user cannot change the direct project ID
@@ -4300,7 +4341,11 @@ function sanitizeChallenge(challenge) {
43004341
]);
43014342
}
43024343
if (challenge.billing) {
4303-
sanitized.billing = _.pick(challenge.billing, ["billingAccountId", "markup"]);
4344+
sanitized.billing = _.pick(challenge.billing, [
4345+
"billingAccountId",
4346+
"markup",
4347+
"clientBillingRate",
4348+
]);
43044349
}
43054350
if (challenge.metadata) {
43064351
sanitized.metadata = _.map(challenge.metadata, (meta) => _.pick(meta, ["name", "value"]));

test/unit/ChallengeService.test.js

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -608,7 +608,10 @@ describe("challenge service unit tests", () => {
608608
createdChallengeData.id,
609609
);
610610

611-
should.equal(result.billing.billingAccountId, createdChallengeData.billing.billingAccountId);
611+
should.equal(
612+
result.billing.billingAccountId,
613+
createdChallengeData.billing.billingAccountId,
614+
);
612615
should.equal(_.isUndefined(result.billing.markup), true);
613616
} finally {
614617
helper.userHasProjectWriteAccess = originalUserHasProjectWriteAccess;
@@ -1656,6 +1659,118 @@ describe("challenge service unit tests", () => {
16561659
should.equal(testHelper.getDatesDiff(result.startDate, testChallengeData.startDate), 0);
16571660
});
16581661

1662+
it("backfills missing billing and locks draft budget including copilot prizes", async () => {
1663+
const challengeData = _.cloneDeep(testChallengeData);
1664+
challengeData.name = `${challengeData.name} Billing Lock ${Date.now()}`;
1665+
challengeData.legacyId = Math.floor(Math.random() * 1000000);
1666+
challengeData.status = ChallengeStatusEnum.NEW;
1667+
challengeData.prizeSets = [
1668+
{
1669+
type: PrizeSetTypeEnum.PLACEMENT,
1670+
description: "placement prizes",
1671+
prizes: [
1672+
{
1673+
description: "placement 1",
1674+
type: constants.prizeTypes.USD,
1675+
value: 1000,
1676+
},
1677+
],
1678+
},
1679+
{
1680+
type: PrizeSetTypeEnum.COPILOT,
1681+
description: "copilot payment",
1682+
prizes: [
1683+
{
1684+
description: "copilot",
1685+
type: constants.prizeTypes.USD,
1686+
value: 150,
1687+
},
1688+
],
1689+
},
1690+
];
1691+
1692+
const originalGetProject = projectHelper.getProject;
1693+
const originalGetProjectBillingInformation = projectHelper.getProjectBillingInformation;
1694+
let billingLookupCount = 0;
1695+
let createdChallengeId;
1696+
1697+
projectHelper.getProject = async () => ({ directProjectId: "33541" });
1698+
projectHelper.getProjectBillingInformation = async () => {
1699+
billingLookupCount += 1;
1700+
1701+
if (billingLookupCount === 1) {
1702+
return {
1703+
billingAccountId: null,
1704+
markup: null,
1705+
};
1706+
}
1707+
1708+
return {
1709+
billingAccountId: "80001012",
1710+
markup: 0.1,
1711+
};
1712+
};
1713+
1714+
try {
1715+
const created = await service.createChallenge(
1716+
{ isMachine: true, sub: "sub-billing-lock-create", userId: "testuser" },
1717+
challengeData,
1718+
config.M2M_FULL_ACCESS_TOKEN,
1719+
);
1720+
createdChallengeId = created.id;
1721+
should.equal(billingLockRequests.length, 0);
1722+
1723+
const draft = await service.updateChallenge(
1724+
{ isMachine: true, sub: "sub-billing-lock-update", userId: 22838965 },
1725+
created.id,
1726+
{
1727+
status: ChallengeStatusEnum.DRAFT,
1728+
},
1729+
);
1730+
1731+
should.equal(draft.billing.billingAccountId, "80001012");
1732+
should.equal(billingLockRequests.length, 1);
1733+
billingLockRequests[0].should.deep.equal({
1734+
billingAccountId: "80001012",
1735+
challengeId: created.id,
1736+
markup: 0.1,
1737+
memberPaymentAmount: 1150,
1738+
});
1739+
1740+
const updatedPrizeSets = _.cloneDeep(draft.prizeSets);
1741+
const copilotPrizeSet = _.find(
1742+
updatedPrizeSets,
1743+
(prizeSet) => _.toString(prizeSet.type).toUpperCase() === PrizeSetTypeEnum.COPILOT,
1744+
);
1745+
should.exist(copilotPrizeSet);
1746+
copilotPrizeSet.prizes[0].value = 225;
1747+
billingLockRequests = [];
1748+
1749+
await service.updateChallenge(
1750+
{ isMachine: true, sub: "sub-billing-lock-prize-update", userId: 22838965 },
1751+
created.id,
1752+
{
1753+
prizeSets: updatedPrizeSets,
1754+
},
1755+
);
1756+
1757+
should.equal(billingLockRequests.length, 1);
1758+
billingLockRequests[0].should.deep.equal({
1759+
billingAccountId: "80001012",
1760+
challengeId: created.id,
1761+
markup: 0.1,
1762+
memberPaymentAmount: 1225,
1763+
});
1764+
} finally {
1765+
projectHelper.getProject = originalGetProject;
1766+
projectHelper.getProjectBillingInformation = originalGetProjectBillingInformation;
1767+
1768+
if (createdChallengeId) {
1769+
await prisma.challenge.deleteMany({ where: { id: createdChallengeId } });
1770+
}
1771+
}
1772+
}).timeout(10000);
1773+
16591774
it("preserves existing terms when update payload omits the terms field", async () => {
16601775
const challengeData = _.cloneDeep(testChallengeData);
16611776
challengeData.name = `${challengeData.name} Terms ${Date.now()}`;

0 commit comments

Comments
 (0)