Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -2381,6 +2381,7 @@ const reconcileSubmitterResourcesForRound = async ({
challengeId,
memberId: String(identity.memberId),
roleId: submitterRoleId,
sendEmail: false,
};

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,23 @@ const createResourceApiClient = ({
return results;
};

const createSubmitterResource = async ({ challengeId, memberId, roleId = submitterRoleId }) => {
/**
* Creates a submitter resource in the Resource API.
* Historical MM imports can set `sendEmail` to false so backfilled registrations
* do not emit registration notifications for already-completed challenges.
* @param {Object} params request payload
* @param {string} params.challengeId v6 challenge id
* @param {string|number} params.memberId target member id
* @param {string} [params.roleId] resource role id, defaults to the configured submitter role
* @param {boolean} [params.sendEmail] optional Resource API email toggle
* @returns {Promise<Object|null>} created resource payload when the API returns JSON
*/
const createSubmitterResource = async ({
challengeId,
memberId,
roleId = submitterRoleId,
sendEmail,
}) => {
const token = await getAccessToken();
const response = await fetchImpl(normalizedBaseUrl, {
method: "POST",
Expand All @@ -159,6 +175,7 @@ const createResourceApiClient = ({
challengeId,
memberId,
roleId,
...(typeof sendEmail === "boolean" ? { sendEmail } : {}),
}),
});

Expand Down
27 changes: 27 additions & 0 deletions data-migration/test/importHistoricalMarathonMatches.apply.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2292,11 +2292,13 @@ describe("importHistoricalMarathonMatches apply create-path behavior", () => {
challengeId: "challenge-1",
memberId: "2",
roleId: "submitter-role",
sendEmail: false,
});
expect(resourceClient.createSubmitterResource).toHaveBeenCalledWith({
challengeId: "challenge-1",
memberId: "3",
roleId: "submitter-role",
sendEmail: false,
});
expect(result.records).toEqual([
{
Expand Down Expand Up @@ -2425,6 +2427,7 @@ describe("importHistoricalMarathonMatches apply create-path behavior", () => {
challengeId: "challenge-1",
memberId: "1",
roleId: "submitter-role",
sendEmail: false,
});
expect(result.records).toEqual([
{
Expand Down Expand Up @@ -2486,6 +2489,18 @@ describe("importHistoricalMarathonMatches apply create-path behavior", () => {
"COMPLETED"
);
expect(resourceClient.createSubmitterResource).toHaveBeenCalledTimes(2);
expect(resourceClient.createSubmitterResource).toHaveBeenNthCalledWith(1, {
challengeId: "challenge-1",
memberId: "2",
roleId: "submitter-role",
sendEmail: false,
});
expect(resourceClient.createSubmitterResource).toHaveBeenNthCalledWith(2, {
challengeId: "challenge-1",
memberId: "2",
roleId: "submitter-role",
sendEmail: false,
});
expect(result).toEqual({
targetEligibleRegistrants: 1,
existingSubmitterResources: 0,
Expand Down Expand Up @@ -2542,5 +2557,17 @@ describe("importHistoricalMarathonMatches apply create-path behavior", () => {
"challenge-1",
"COMPLETED"
);
expect(resourceClient.createSubmitterResource).toHaveBeenNthCalledWith(1, {
challengeId: "challenge-1",
memberId: "2",
roleId: "submitter-role",
sendEmail: false,
});
expect(resourceClient.createSubmitterResource).toHaveBeenNthCalledWith(2, {
challengeId: "challenge-1",
memberId: "2",
roleId: "submitter-role",
sendEmail: false,
});
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const {
createAuth0TokenProvider,
createResourceApiClient,
} = require("../src/scripts/importHistoricalMarathonMatches/resourceApi");

describe("importHistoricalMarathonMatches resource api auth provider", () => {
Expand All @@ -25,4 +26,41 @@ describe("importHistoricalMarathonMatches resource api auth provider", () => {
expect.objectContaining({ method: "POST" })
);
});

test("forwards sendEmail when creating submitter resources", async () => {
const fetchImpl = jest.fn().mockResolvedValue({
ok: true,
status: 200,
statusText: "OK",
text: async () => JSON.stringify({ id: "resource-1" }),
});

const client = createResourceApiClient({
baseUrl: "https://api.topcoder-dev.com/v6/resources",
submitterRoleId: "submitter-role",
getAccessToken: async () => "token-1",
fetchImpl,
});

await expect(
client.createSubmitterResource({
challengeId: "challenge-1",
memberId: "12345",
sendEmail: false,
})
).resolves.toEqual({ id: "resource-1" });

expect(fetchImpl).toHaveBeenCalledWith(
"https://api.topcoder-dev.com/v6/resources",
expect.objectContaining({
method: "POST",
body: JSON.stringify({
challengeId: "challenge-1",
memberId: "12345",
roleId: "submitter-role",
sendEmail: false,
}),
})
);
});
});
83 changes: 83 additions & 0 deletions src/scripts/backfill-2026-challenge-billing-markups.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
-- Backfill challenge billing markup from the billing-accounts schema.
--
-- Scope:
-- - Challenges created on or after 2026-01-01 00:00:00.
-- - Challenges created before 2027-01-01 00:00:00.
-- - Existing challenge billing records with a billingAccountId.
-- - Rows where the stored challenge markup differs from the billing account
-- markup.
--
-- Run this against the PostgreSQL database that contains both schemas:
-- - "challenges"
-- - "billing-accounts"

BEGIN;

WITH candidates AS (
SELECT
cb."id" AS "challengeBillingId",
cb."challengeId",
cb."billingAccountId",
cb."markup" AS "currentMarkup",
ba."markup"::double precision AS "billingAccountMarkup"
FROM "challenges"."ChallengeBilling" cb
INNER JOIN "challenges"."Challenge" c
ON c."id" = cb."challengeId"
INNER JOIN "billing-accounts"."BillingAccount" ba
ON ba."id"::text = cb."billingAccountId"
WHERE c."createdAt" >= TIMESTAMP '2026-01-01 00:00:00'
AND c."createdAt" < TIMESTAMP '2027-01-01 00:00:00'
AND cb."billingAccountId" IS NOT NULL
AND cb."markup" IS DISTINCT FROM ba."markup"::double precision
)
SELECT
COUNT(*) AS "rowsToUpdate",
COUNT(DISTINCT "billingAccountId") AS "billingAccountsAffected"
FROM candidates;

WITH candidates AS (
SELECT
cb."id" AS "challengeBillingId",
ba."markup"::double precision AS "billingAccountMarkup"
FROM "challenges"."ChallengeBilling" cb
INNER JOIN "challenges"."Challenge" c
ON c."id" = cb."challengeId"
INNER JOIN "billing-accounts"."BillingAccount" ba
ON ba."id"::text = cb."billingAccountId"
WHERE c."createdAt" >= TIMESTAMP '2026-01-01 00:00:00'
AND c."createdAt" < TIMESTAMP '2027-01-01 00:00:00'
AND cb."billingAccountId" IS NOT NULL
AND cb."markup" IS DISTINCT FROM ba."markup"::double precision
),
updated AS (
UPDATE "challenges"."ChallengeBilling" cb
SET
"markup" = candidates."billingAccountMarkup",
"updatedAt" = CURRENT_TIMESTAMP,
"updatedBy" = 'billing-markup-backfill-2026'
FROM candidates
WHERE cb."id" = candidates."challengeBillingId"
RETURNING
cb."challengeId",
cb."billingAccountId",
cb."markup"
)
SELECT
COUNT(*) AS "rowsUpdated",
COUNT(DISTINCT "billingAccountId") AS "billingAccountsAffected"
FROM updated;

-- Example spot-check for the challenge from the incident:
SELECT
c."id" AS "challengeId",
cb."billingAccountId",
cb."markup" AS "challengeMarkup",
ba."markup" AS "billingAccountMarkup"
FROM "challenges"."Challenge" c
INNER JOIN "challenges"."ChallengeBilling" cb
ON cb."challengeId" = c."id"
INNER JOIN "billing-accounts"."BillingAccount" ba
ON ba."id"::text = cb."billingAccountId"
WHERE c."id" = '57a1d424-1931-49a8-a180-d0b0f6cdf293';

COMMIT;
Loading