From dd7e68f286d568401c9354364e96330d707f09e9 Mon Sep 17 00:00:00 2001 From: Rakib Ansary Date: Tue, 14 Nov 2023 19:12:45 +0600 Subject: [PATCH 01/12] feat: introduce point reward type Signed-off-by: Rakib Ansary --- package.json | 6 +- src/domain/Challenge.ts | 87 ++++++++++++------- .../domain-layer/challenge/challenge.ts | 35 ++++++-- src/util/LegacyMapper.ts | 30 +++++-- yarn.lock | 44 +++++----- 5 files changed, 130 insertions(+), 72 deletions(-) diff --git a/package.json b/package.json index c374b91..694fcef 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,9 @@ "@aws-sdk/util-utf8-node": "^3.259.0", "@grpc/grpc-js": "^1.7.1", "@opensearch-project/opensearch": "^2.2.0", - "@topcoder-framework/domain-acl": "0.24", - "@topcoder-framework/lib-common": "0.24", - "topcoder-proto-registry": "0.1.0", + "@topcoder-framework/domain-acl": "0.24.1", + "@topcoder-framework/lib-common": "0.24.1", + "topcoder-proto-registry": "0.2.0", "@types/uuid": "^9.0.1", "aws-sdk": "^2.1339.0", "axios": "^1.2.2", diff --git a/src/domain/Challenge.ts b/src/domain/Challenge.ts index 8027777..6363428 100644 --- a/src/domain/Challenge.ts +++ b/src/domain/Challenge.ts @@ -150,6 +150,8 @@ class ChallengeDomain extends CoreOperations { // prettier-ignore const handle = metadata?.get("handle").length > 0 ? metadata?.get("handle")?.[0].toString() : "tcwebservice"; + const prizeType: "USD" | "POINT" = + (input.prizeSets?.[0]?.prizes?.[0]?.type as "USD" | "POINT") ?? null; if (Array.isArray(input.discussions)) { for (const discussion of input.discussions) { @@ -160,10 +162,13 @@ class ChallengeDomain extends CoreOperations { const track = V5_TRACK_IDS_TO_NAMES[input.trackId]; const type = V5_TYPE_IDS_TO_NAMES[input.typeId]; - const estimatedTotalInCents = new ChallengeEstimator(input.prizeSets ?? [], { - track, - type, - }).estimateCost(EXPECTED_REVIEWS_PER_REVIEWER, NUM_REVIEWERS); + const estimatedTotalInCents = + prizeType === "USD" + ? new ChallengeEstimator(input.prizeSets ?? [], { + track, + type, + }).estimateCost(EXPECTED_REVIEWS_PER_REVIEWER, NUM_REVIEWERS) + : null; const now = new Date().getTime(); const challengeId = IdGenerator.generateUUID(); @@ -178,15 +183,19 @@ class ChallengeDomain extends CoreOperations { } // Lock amount - const baValidation: BAValidation = { - billingAccountId: input.billing?.billingAccountId, - challengeId, - markup: baValidationMarkup, - status: input.status, - totalPrizesInCents: estimatedTotalInCents, - prevTotalPrizesInCents: 0, - }; - await lockConsumeAmount(baValidation); + const baValidation: BAValidation | null = + estimatedTotalInCents != null + ? { + billingAccountId: input.billing?.billingAccountId, + challengeId, + markup: baValidationMarkup, + status: input.status, + totalPrizesInCents: estimatedTotalInCents, + prevTotalPrizesInCents: 0, + } + : null; + + if (baValidation != null) await lockConsumeAmount(baValidation); let newChallenge: Challenge; try { @@ -197,11 +206,11 @@ class ChallengeDomain extends CoreOperations { // End Anti-Corruption Layer - const totalPlacementPrizeInCents = _.sumBy( + const totalPlacementPrizes = _.sumBy( _.find(input.prizeSets ?? [], { type: PrizeSetTypes.ChallengePrizes, })?.prizes ?? [], - "amountInCents" + prizeType === "USD" ? "amountInCents" : "value" ); const challenge: Challenge = { @@ -214,8 +223,9 @@ class ChallengeDomain extends CoreOperations { winners: [], payments: [], overview: { - totalPrizes: totalPlacementPrizeInCents / 100, - totalPrizesInCents: totalPlacementPrizeInCents, + type: prizeType, + totalPrizes: type === "USD" ? totalPlacementPrizes / 100 : totalPlacementPrizes, + totalPrizesInCents: type === "USD" ? totalPlacementPrizes : undefined, }, ...input, prizeSets: (input.prizeSets ?? []).map((prizeSet) => { @@ -224,14 +234,14 @@ class ChallengeDomain extends CoreOperations { prizes: (prizeSet.prizes ?? []).map((prize) => { return { ...prize, - value: prize.amountInCents! / 100, + value: prizeType === "USD" ? prize.amountInCents! / 100 : prize.value, }; }), }; }), legacy, phases, - legacyId: legacyChallengeId != null ? legacyChallengeId : undefined, + legacyId: legacyChallengeId ?? undefined, description: sanitize(input.description ?? "", input.descriptionFormat), privateDescription: sanitize(input.privateDescription ?? "", input.descriptionFormat), metadata: @@ -252,7 +262,9 @@ class ChallengeDomain extends CoreOperations { newChallenge = await super.create(challenge, metadata); } catch (err) { // Rollback lock amount - await lockConsumeAmount(baValidation, true); + if (baValidation != null) { + await lockConsumeAmount(baValidation, true); + } throw err; } @@ -276,18 +288,32 @@ class ChallengeDomain extends CoreOperations { const track = V5_TRACK_IDS_TO_NAMES[challenge.trackId]; const type = V5_TYPE_IDS_TO_NAMES[challenge.typeId]; + const existingPrizeType: string | null = challenge?.prizeSets?.[0]?.prizes?.[0]?.type ?? null; + const prizeType: string | null = + input.prizeSetUpdate?.prizeSets?.[0]?.prizes?.[0]?.type ?? null; + + if (existingPrizeType !== prizeType) { + throw new StatusBuilder() + .withCode(Status.INVALID_ARGUMENT) + .withDetails("Prize type can not be changed") + .build(); + } + let shouldLockBudget = input.prizeSetUpdate != null; const isCancelled = input.status?.toLowerCase().indexOf("cancelled") !== -1; let generatePayments = false; let baValidation: BAValidation | null = null; // Lock budget only if prize set is updated - const prevTotalPrizesInCents = new ChallengeEstimator(challenge?.prizeSets ?? [], { - track, - type, - }).estimateCost(EXPECTED_REVIEWS_PER_REVIEWER, NUM_REVIEWERS); // These are estimates, fetch reviewer number using constraint in review phase + const prevTotalPrizesInCents = + prizeType == "USD" + ? new ChallengeEstimator(challenge?.prizeSets ?? [], { + track, + type, + }).estimateCost(EXPECTED_REVIEWS_PER_REVIEWER, NUM_REVIEWERS) // These are estimates, fetch reviewer number using constraint in review phase + : 0; - if (shouldLockBudget || isCancelled) { + if (prizeType === "USD" && (shouldLockBudget || isCancelled)) { const totalPrizesInCents = _.isArray(input.prizeSetUpdate?.prizeSets) ? new ChallengeEstimator(input.prizeSetUpdate?.prizeSets! ?? [], { track, @@ -311,11 +337,11 @@ class ChallengeDomain extends CoreOperations { await lockConsumeAmount(baValidation); } - const totalPlacementPrizeInCents = _.sumBy( + const totalPlacementPrize = _.sumBy( _.find(input.prizeSetUpdate?.prizeSets ?? [], { type: PrizeSetTypes.ChallengePrizes, })?.prizes ?? [], - "amountInCents" + prizeType == "USD" ? "amountInCents" : "value" ); try { @@ -482,7 +508,7 @@ class ChallengeDomain extends CoreOperations { prizes: (prizeSet.prizes ?? []).map((prize) => { return { ...prize, - value: prize.amountInCents! / 100, + value: prizeType == 'USD' ? prize.amountInCents! / 100 : prize.value, }; }), }; @@ -496,8 +522,9 @@ class ChallengeDomain extends CoreOperations { startDate: input.startDate ?? undefined, endDate: input?.status === ChallengeStatuses.Completed ? new Date().toISOString() : (input.endDate ?? undefined), overview: input.overview != null ? { - totalPrizes: totalPlacementPrizeInCents / 100, - totalPrizesInCents: totalPlacementPrizeInCents, + type: prizeType, + totalPrizes: prizeType == 'USD' ? totalPlacementPrize / 100 : totalPlacementPrize, + totalPrizesInCents: prizeType == 'USD' ? totalPlacementPrize: undefined, } : undefined, legacyId: legacyId ?? undefined, constraints: input.constraints ?? undefined, diff --git a/src/models/domain-layer/challenge/challenge.ts b/src/models/domain-layer/challenge/challenge.ts index 2a4559e..d7f5468 100644 --- a/src/models/domain-layer/challenge/challenge.ts +++ b/src/models/domain-layer/challenge/challenge.ts @@ -144,7 +144,11 @@ export interface Challenge_PrizeSet_Prize { export interface Challenge_Overview { totalPrizesInCents?: number | undefined; - totalPrizes?: number | undefined; + totalPrizes?: + | number + | undefined; + /** USD, POINT */ + type?: string | undefined; } export interface Challenge_Constraint { @@ -2336,7 +2340,7 @@ export const Challenge_PrizeSet_Prize = { writer.uint32(8).int64(message.amountInCents); } if (message.value !== undefined) { - writer.uint32(21).float(message.value); + writer.uint32(17).double(message.value); } if (message.type !== "") { writer.uint32(26).string(message.type); @@ -2359,11 +2363,11 @@ export const Challenge_PrizeSet_Prize = { message.amountInCents = longToNumber(reader.int64() as Long); continue; case 2: - if (tag !== 21) { + if (tag !== 17) { break; } - message.value = reader.float(); + message.value = reader.double(); continue; case 3: if (tag !== 26) { @@ -2416,7 +2420,7 @@ export const Challenge_PrizeSet_Prize = { }; function createBaseChallenge_Overview(): Challenge_Overview { - return { totalPrizesInCents: undefined, totalPrizes: undefined }; + return { totalPrizesInCents: undefined, totalPrizes: undefined, type: undefined }; } export const Challenge_Overview = { @@ -2425,7 +2429,10 @@ export const Challenge_Overview = { writer.uint32(8).int64(message.totalPrizesInCents); } if (message.totalPrizes !== undefined) { - writer.uint32(21).float(message.totalPrizes); + writer.uint32(17).double(message.totalPrizes); + } + if (message.type !== undefined) { + writer.uint32(26).string(message.type); } return writer; }, @@ -2445,11 +2452,18 @@ export const Challenge_Overview = { message.totalPrizesInCents = longToNumber(reader.int64() as Long); continue; case 2: - if (tag !== 21) { + if (tag !== 17) { + break; + } + + message.totalPrizes = reader.double(); + continue; + case 3: + if (tag !== 26) { break; } - message.totalPrizes = reader.float(); + message.type = reader.string(); continue; } if ((tag & 7) === 4 || tag === 0) { @@ -2464,6 +2478,7 @@ export const Challenge_Overview = { return { totalPrizesInCents: isSet(object.totalPrizesInCents) ? globalThis.Number(object.totalPrizesInCents) : undefined, totalPrizes: isSet(object.totalPrizes) ? globalThis.Number(object.totalPrizes) : undefined, + type: isSet(object.type) ? globalThis.String(object.type) : undefined, }; }, @@ -2475,6 +2490,9 @@ export const Challenge_Overview = { if (message.totalPrizes !== undefined) { obj.totalPrizes = message.totalPrizes; } + if (message.type !== undefined) { + obj.type = message.type; + } return obj; }, @@ -2485,6 +2503,7 @@ export const Challenge_Overview = { const message = createBaseChallenge_Overview(); message.totalPrizesInCents = object.totalPrizesInCents ?? undefined; message.totalPrizes = object.totalPrizes ?? undefined; + message.type = object.type ?? undefined; return message; }, }; diff --git a/src/util/LegacyMapper.ts b/src/util/LegacyMapper.ts index 7ec33d4..fcc8960 100644 --- a/src/util/LegacyMapper.ts +++ b/src/util/LegacyMapper.ts @@ -31,7 +31,8 @@ class LegacyMapper { input: CreateChallengeInput, id: string ): Promise => { - const prizeSets = this.mapPrizeSets(input.prizeSets); + const prizeType = input.prizeSets[0]?.prizes[0]?.type; + const prizeSets = prizeType === "USD" ? this.mapPrizeSets(input.prizeSets) : null; const projectInfo = this.mapProjectInfo(input, prizeSets, input.legacy?.subTrack!); return { @@ -43,7 +44,7 @@ class LegacyMapper { ), groups: await this.mapGroupIds(input.groups), tcDirectProjectId: input.legacy?.directProjectId!, - winnerPrizes: this.mapWinnerPrizes(prizeSets), + winnerPrizes: prizeSets == null ? [] : this.mapWinnerPrizes(prizeSets), phases: this.mapPhases( input.legacy!.subTrack!, input.billing?.billingAccountId, @@ -62,8 +63,10 @@ class LegacyMapper { billingAccount: number | undefined, input: UpdateChallengeInput_UpdateInput ): Promise => { + const prizeType = input.prizeSetUpdate?.prizeSets[0]?.prizes[0]?.type; + // prettier-ignore - const prizeSets = input.prizeSetUpdate != null ? this.mapPrizeSets(input.prizeSetUpdate.prizeSets) : null; + const prizeSets = input.prizeSetUpdate != null && prizeType === 'USD' ? this.mapPrizeSets(input.prizeSetUpdate.prizeSets) : null; const projectInfo = this.mapProjectInfoForUpdate(input, prizeSets); return { @@ -147,8 +150,11 @@ class LegacyMapper { prizeSets: any, subTrack: string ): { [key: number]: string } { + const prizeType = input.prizeSets[0]?.prizes[0]?.type; + const isMemberPaymentEligible = prizeType === "USD"; + const firstPlacePrize = - prizeSets[PrizeSetTypes.ChallengePrizes]?.length >= 1 + isMemberPaymentEligible && prizeSets[PrizeSetTypes.ChallengePrizes]?.length >= 1 ? prizeSets[PrizeSetTypes.ChallengePrizes][0]?.toString() : undefined; @@ -163,7 +169,10 @@ class LegacyMapper { 12: "Yes", // Public -> Yes (make it dynamic) 13: "Yes", // Rated -> Yes (make it dynamic) 14: "Open", // Eligibility -> Open (value doesn't matter) - 16: firstPlacePrize != null ? (firstPlacePrize / 100).toString() : undefined, + 16: + isMemberPaymentEligible && firstPlacePrize != null + ? (firstPlacePrize / 100).toString() + : undefined, 26: "Off", // No Digital Run 28: [ @@ -179,7 +188,7 @@ class LegacyMapper { 32: input.billing?.billingAccountId!.toString(), // Review Cost 33: - prizeSets[PrizeSetTypes.ReviewerPayment]?.length == 1 + isMemberPaymentEligible && prizeSets[PrizeSetTypes.ReviewerPayment]?.length == 1 ? (prizeSets[PrizeSetTypes.ReviewerPayment][0] / 100).toString() : undefined, // Confidentiality Type @@ -189,10 +198,13 @@ class LegacyMapper { // Spec Review Cost 35: undefined, // First Place Prize - 36: firstPlacePrize != null ? (firstPlacePrize / 100).toString() : undefined, + 36: + isMemberPaymentEligible && firstPlacePrize != null + ? (firstPlacePrize / 100).toString() + : undefined, // Second Place Prize 37: - prizeSets[PrizeSetTypes.ChallengePrizes]?.length >= 2 + isMemberPaymentEligible && prizeSets[PrizeSetTypes.ChallengePrizes]?.length >= 2 ? (prizeSets[PrizeSetTypes.ChallengePrizes][1] / 100).toString() : undefined, // Reliability Bonus Cost @@ -206,7 +218,7 @@ class LegacyMapper { ? "false" : "true", // Post-mortem required (set to false - new Autopilot will handle this) 45: "false", // Reliability bonus eligible - 46: "true", // Member Payments Eligible + 46: isMemberPaymentEligible ? "true" : "false", // Member Payments Eligible 48: "false", // Track Late Deliverables 52: "false", // Allow Stock Art 57: input.billing?.markup!.toString(), // Contest Fee Percentage diff --git a/yarn.lock b/yarn.lock index 386defe..06f4a5f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -986,35 +986,35 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== -"@topcoder-framework/client-relational@^0.24.0": - version "0.24.0" - resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com/npm/topcoder-framework/@topcoder-framework/client-relational/-/client-relational-0.24.0.tgz#07ee26b1a35f3a5c4c7d7f2ec7300efbc63cf311" - integrity sha512-H+sHV9pKDW6urMZLr//NLmC2XDX6obrEy1dynpmF2W1AgkMFDgWy9euevAjPNWqnZvScW08N3c8yPwQ43/akaw== +"@topcoder-framework/client-relational@^0.24.1": + version "0.24.1" + resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com/npm/topcoder-framework/@topcoder-framework/client-relational/-/client-relational-0.24.1.tgz#a01c4bb5a6117c38d5b61e1a79ded58abeb5b7c5" + integrity sha512-yZJS2N6l1YT/wadWRgMhMzrtFNfPgCBMVLUPO2ORHq182xXSIBkpXLRvX99z2XVPRGpjCVkfumh+jANQKX8AXw== dependencies: "@grpc/grpc-js" "^1.8.0" - "@topcoder-framework/lib-common" "^0.24.0" - topcoder-proto-registry "0.1.0" + "@topcoder-framework/lib-common" "^0.24.1" + topcoder-proto-registry "0.2.0" tslib "^2.4.1" -"@topcoder-framework/domain-acl@0.24": - version "0.24.0" - resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com/npm/topcoder-framework/@topcoder-framework/domain-acl/-/domain-acl-0.24.0.tgz#268fd88e076b841a5a2b18c262aae791758d728e" - integrity sha512-gIwrbVSbc9mT30J0H+ewPcq6krHtBO3njLfPLMSN4RKw6YRu/LkNepdlXBUuuRQWvWK/zeBnNpnpZVfAuDKcnA== +"@topcoder-framework/domain-acl@0.24.1": + version "0.24.1" + resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com/npm/topcoder-framework/@topcoder-framework/domain-acl/-/domain-acl-0.24.1.tgz#61435da8d28756a319bc31bfd4f5b4458614f64f" + integrity sha512-7+uvCxkfURuXsBIwQBvn9aBobBheZCKlKQWzH4Dk5sCBxyhbjQ0B+TkFnfn2+65NzblwuvYEAEnJdLyAYu4RPA== dependencies: "@grpc/grpc-js" "^1.8.7" - "@topcoder-framework/client-relational" "^0.24.0" - "@topcoder-framework/lib-common" "^0.24.0" - topcoder-proto-registry "0.1.0" + "@topcoder-framework/client-relational" "^0.24.1" + "@topcoder-framework/lib-common" "^0.24.1" + topcoder-proto-registry "0.2.0" tslib "^2.4.1" -"@topcoder-framework/lib-common@0.24", "@topcoder-framework/lib-common@^0.24.0": - version "0.24.0" - resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com/npm/topcoder-framework/@topcoder-framework/lib-common/-/lib-common-0.24.0.tgz#a6baeeb3e1d3ad15cd0c010aaa7a62769a908a90" - integrity sha512-lKTqHYyVRdSs8JvXT9MYpxe4bY/ysIksCvj7OJUtntExkRjv7EPBysofjb7H4uNsofa2G5iOGycJtp+Qhaqb8Q== +"@topcoder-framework/lib-common@0.24.1", "@topcoder-framework/lib-common@^0.24.1": + version "0.24.1" + resolved "https://topcoder-409275337247.d.codeartifact.us-east-1.amazonaws.com/npm/topcoder-framework/@topcoder-framework/lib-common/-/lib-common-0.24.1.tgz#fc69af0f3deb263d347bfb8ac014065c5a7ceeec" + integrity sha512-Av/v5YybzyrJlhxANFxy+uJR938OWzd4vkcBZvAWmY4wX9D8UOiBA1nF2EMZ5+9xhY+PD3O/yuqnfqUs/4qT+g== dependencies: "@grpc/grpc-js" "^1.8.0" rimraf "^3.0.2" - topcoder-proto-registry "0.1.0" + topcoder-proto-registry "0.2.0" tslib "^2.4.1" "@tsconfig/node10@^1.0.7": @@ -2714,10 +2714,10 @@ topcoder-bus-api-wrapper@topcoder-platform/tc-bus-api-wrapper.git: superagent "^3.8.3" tc-core-library-js appirio-tech/tc-core-library-js.git#v2.6.4 -topcoder-proto-registry@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/topcoder-proto-registry/-/topcoder-proto-registry-0.1.0.tgz#7bdcb7df7c8bbf9d54beba1c69a6210d0f4ca097" - integrity sha512-2RYGdDfCaX02pNcJu7ofb26O0SPe4MA6yfvpzXx6DjiuGtZu5QSZHkeaxqAlzRc9/F5zfWmGJwin4TOppo2xrA== +topcoder-proto-registry@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/topcoder-proto-registry/-/topcoder-proto-registry-0.2.0.tgz#703d636d2581b7b3903fe299f6c3d572c5f728c0" + integrity sha512-qmoAY0jb25A4S4bunUagj+wP++d1Db0iZqMc0SaMFjzW33dXjay7TpJDBbNZuVk4He7kUhYXrn2CDikbPM3TFw== topo@3.x.x: version "3.0.3" From 6a0a6b6c1c715e8b59d9ce6e4ca1ef5626dd8c97 Mon Sep 17 00:00:00 2001 From: Rakib Ansary Date: Tue, 14 Nov 2023 19:14:47 +0600 Subject: [PATCH 02/12] feat: introduce point reward type Signed-off-by: Rakib Ansary --- src/domain/Challenge.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/domain/Challenge.ts b/src/domain/Challenge.ts index 6363428..3b773b4 100644 --- a/src/domain/Challenge.ts +++ b/src/domain/Challenge.ts @@ -794,6 +794,11 @@ class ChallengeDomain extends CoreOperations { title: string, payments: UpdateChallengeInputForACL_PaymentACL[] ): Promise { + console.log( + `Generating payments for challenge ${challengeId}, ${title} with payments ${JSON.stringify( + payments + )}` + ); let totalAmount = 0; // TODO: Make this list exhaustive const mapType = (type: string) => { From 6848d1cae8a9c428e31b73e80528ceaa630d11f5 Mon Sep 17 00:00:00 2001 From: Rakib Ansary Date: Tue, 14 Nov 2023 19:33:19 +0600 Subject: [PATCH 03/12] fix: add type in schema Signed-off-by: Rakib Ansary --- src/domain/Challenge.ts | 2 +- src/schema/Challenge.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/domain/Challenge.ts b/src/domain/Challenge.ts index 3b773b4..d7b4552 100644 --- a/src/domain/Challenge.ts +++ b/src/domain/Challenge.ts @@ -292,7 +292,7 @@ class ChallengeDomain extends CoreOperations { const prizeType: string | null = input.prizeSetUpdate?.prizeSets?.[0]?.prizes?.[0]?.type ?? null; - if (existingPrizeType !== prizeType) { + if (existingPrizeType != null && prizeType != null && existingPrizeType !== prizeType) { throw new StatusBuilder() .withCode(Status.INVALID_ARGUMENT) .withDetails("Prize type can not be changed") diff --git a/src/schema/Challenge.ts b/src/schema/Challenge.ts index 3fda157..707f6a3 100644 --- a/src/schema/Challenge.ts +++ b/src/schema/Challenge.ts @@ -160,6 +160,7 @@ export const ChallengeSchema: Schema = { type: DataType.DATA_TYPE_MAP, itemType: DataType.DATA_TYPE_MAP, items: { + type: { type: DataType.DATA_TYPE_STRING }, totalPrizes: { type: DataType.DATA_TYPE_NUMBER, format: "float", precision: 2 }, totalPrizesInCents: { type: DataType.DATA_TYPE_NUMBER, format: "integer" }, }, From f485f9ac3c10d251cbfc9832652d582d6a06b641 Mon Sep 17 00:00:00 2001 From: Rakib Ansary Date: Tue, 14 Nov 2023 20:44:25 +0600 Subject: [PATCH 04/12] fix: include non monetary placements in winners Signed-off-by: Rakib Ansary --- src/domain/Challenge.ts | 53 ++++++++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/src/domain/Challenge.ts b/src/domain/Challenge.ts index d7b4552..74fc4d4 100644 --- a/src/domain/Challenge.ts +++ b/src/domain/Challenge.ts @@ -585,6 +585,7 @@ class ChallengeDomain extends CoreOperations { const data: IUpdateDataFromACL = {}; const challenge = await this.lookup(DomainHelper.getLookupCriteria("id", id)); + const prizeType = challenge?.prizeSets?.[0]?.prizes?.[0]?.type; let raiseEvent = false; @@ -637,7 +638,37 @@ class ChallengeDomain extends CoreOperations { data.winners = input.winners.winners; if (!_.isUndefined(input.payments)) { - data.payments = input.payments.payments; + if (prizeType === "USD") { + data.payments = input.payments.payments; + } else { + data.payments = data.winners.map((winner) => { + const placementPrizes = challenge.prizeSets.find( + (p) => p.type === PrizeSetTypes.ChallengePrizes + )?.prizes; + + return { + amount: placementPrizes![winner.placement! - 1].amountInCents! / 100 ?? 0, + type: "CONTEST_PAYMENT", + userId: winner.userId, + handle: winner.handle, + }; + }); + + const reviewerPayments = input.payments.payments + .filter((f) => f.type === "iterative reviewer") + .map((p) => ({ + amount: p.amount, + type: "REVIEW_BOARD_PAYMENT", + userId: p.userId, + handle: p.handle, + })); + + if (reviewerPayments.length > 0) { + data.payments = data.payments.concat(reviewerPayments); + } + + console.log("Final list of payments", data.payments); + } } raiseEvent = true; @@ -695,15 +726,19 @@ class ChallengeDomain extends CoreOperations { const completedChallenge = await this.lookup(DomainHelper.getLookupCriteria("id", id)); - console.log("Payments to Generate", completedChallenge.payments); - const totalAmount = await this.generatePayments( - completedChallenge.id, - completedChallenge.name, - completedChallenge.payments - ); + if (prizeType == "USD") { + console.log("Payments to Generate", completedChallenge.payments); + const totalAmount = await this.generatePayments( + completedChallenge.id, + completedChallenge.name, + completedChallenge.payments + ); - baValidation.totalPrizesInCents = totalAmount * 100; - await lockConsumeAmount(baValidation); + baValidation.totalPrizesInCents = totalAmount * 100; + await lockConsumeAmount(baValidation); + } else { + console.log("Need to generate POINTS"); + } } if (input.phases?.phases && input.phases.phases.length && this.shouldUseScheduler(challenge!)) { From 608ec0f7e774068c2e08e08fbd7f8b8004893664 Mon Sep 17 00:00:00 2001 From: Rakib Ansary Date: Tue, 14 Nov 2023 20:45:14 +0600 Subject: [PATCH 05/12] chore: add logs Signed-off-by: Rakib Ansary --- src/domain/Challenge.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/domain/Challenge.ts b/src/domain/Challenge.ts index 74fc4d4..7f3f77f 100644 --- a/src/domain/Challenge.ts +++ b/src/domain/Challenge.ts @@ -641,13 +641,14 @@ class ChallengeDomain extends CoreOperations { if (prizeType === "USD") { data.payments = input.payments.payments; } else { + console.log("Point Winners", data.winners); data.payments = data.winners.map((winner) => { const placementPrizes = challenge.prizeSets.find( (p) => p.type === PrizeSetTypes.ChallengePrizes )?.prizes; return { - amount: placementPrizes![winner.placement! - 1].amountInCents! / 100 ?? 0, + amount: placementPrizes![winner.placement! - 1].amountInCents! / 100, type: "CONTEST_PAYMENT", userId: winner.userId, handle: winner.handle, From 8a8b29a555d2462390e456596b1b92d99c52af65 Mon Sep 17 00:00:00 2001 From: Rakib Ansary Date: Tue, 14 Nov 2023 21:09:25 +0600 Subject: [PATCH 06/12] ci: CORE-40 -> point reward type --- src/domain/Challenge.ts | 53 ++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/src/domain/Challenge.ts b/src/domain/Challenge.ts index 7f3f77f..c1fd0be 100644 --- a/src/domain/Challenge.ts +++ b/src/domain/Challenge.ts @@ -648,7 +648,7 @@ class ChallengeDomain extends CoreOperations { )?.prizes; return { - amount: placementPrizes![winner.placement! - 1].amountInCents! / 100, + amount: placementPrizes?.[winner.placement - 1]?.value ?? 0, type: "CONTEST_PAYMENT", userId: winner.userId, handle: winner.handle, @@ -691,34 +691,37 @@ class ChallengeDomain extends CoreOperations { const track = V5_TRACK_IDS_TO_NAMES[challenge?.trackId ?? ""]; const type = V5_TYPE_IDS_TO_NAMES[challenge?.typeId ?? ""]; - const prevTotalPrizesInCents = new ChallengeEstimator(challenge?.prizeSets ?? [], { - track, - type, - }).estimateCost(EXPECTED_REVIEWS_PER_REVIEWER, NUM_REVIEWERS); // These are estimates, fetch reviewer number using constraint in review phase + let baValidation: BAValidation | null = null; - const totalPrizesInCents = _.isArray(data.prizeSets) - ? new ChallengeEstimator(data.prizeSets ?? [], { - track, - type, - }).estimateCost(EXPECTED_REVIEWS_PER_REVIEWER, NUM_REVIEWERS) // These are estimates, fetch reviewer number using constraint in review phase - : prevTotalPrizesInCents; + if (prizeType === "USD") { + const prevTotalPrizesInCents = new ChallengeEstimator(challenge?.prizeSets ?? [], { + track, + type, + }).estimateCost(EXPECTED_REVIEWS_PER_REVIEWER, NUM_REVIEWERS); // These are estimates, fetch reviewer number using constraint in review phase + const totalPrizesInCents = _.isArray(data.prizeSets) + ? new ChallengeEstimator(data.prizeSets ?? [], { + track, + type, + }).estimateCost(EXPECTED_REVIEWS_PER_REVIEWER, NUM_REVIEWERS) // These are estimates, fetch reviewer number using constraint in review phase + : prevTotalPrizesInCents; - const baValidation: BAValidation = { - challengeId: challenge?.id, - billingAccountId: challenge?.billing?.billingAccountId, - markup: challenge?.billing?.markup, - status: challengeStatus, - prevStatus: challenge?.status, - totalPrizesInCents, - prevTotalPrizesInCents, - }; + baValidation = { + challengeId: challenge?.id, + billingAccountId: challenge?.billing?.billingAccountId, + markup: challenge?.billing?.markup, + status: challengeStatus, + prevStatus: challenge?.status, + totalPrizesInCents, + prevTotalPrizesInCents, + }; + } if (challengeStatus != ChallengeStatuses.Completed) { - await lockConsumeAmount(baValidation); + if (baValidation != null) await lockConsumeAmount(baValidation); try { await super.update(scanCriteria, dynamoUpdate); } catch (err) { - await lockConsumeAmount(baValidation, true); + if (baValidation != null) await lockConsumeAmount(baValidation, true); throw err; } } else { @@ -735,8 +738,10 @@ class ChallengeDomain extends CoreOperations { completedChallenge.payments ); - baValidation.totalPrizesInCents = totalAmount * 100; - await lockConsumeAmount(baValidation); + if (baValidation != null) { + baValidation.totalPrizesInCents = totalAmount * 100; + await lockConsumeAmount(baValidation); + } } else { console.log("Need to generate POINTS"); } From 8ae54ae1387e4c2f2468805153cd014bdf61c991 Mon Sep 17 00:00:00 2001 From: Rakib Ansary Date: Fri, 24 Nov 2023 12:24:36 +0600 Subject: [PATCH 07/12] feat: pure-v5 challenge * this is a stopgap solution till we introduce * a "config" object in either timeline-template * configuration or elsewhere - such as for track/type combo Signed-off-by: Rakib Ansary --- src/domain/Challenge.ts | 32 +++++++++++++++++++++++--------- src/util/LegacyMapper.ts | 9 ++++++++- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/domain/Challenge.ts b/src/domain/Challenge.ts index c1fd0be..8dcb449 100644 --- a/src/domain/Challenge.ts +++ b/src/domain/Challenge.ts @@ -56,6 +56,9 @@ const legacyChallengeDomain = new LegacyChallengeDomain( const NUM_REVIEWERS = 2; const EXPECTED_REVIEWS_PER_REVIEWER = 3; const ROLE_COPILOT = process.env.ROLE_COPILOT ?? "cfe12b3f-2a24-4639-9d8b-ec86726f76bd"; +const PURE_V5_CHALLENGE_TEMPLATE_IDS = process.env.PURE_V5_CHALLENGE_TEMPLATE_IDS + ? JSON.parse(process.env.PURE_V5_CHALLENGE_TEMPLATE_IDS) + : ["517e76b0-8824-4e72-9b48-a1ebde1793a8"]; class ChallengeDomain extends CoreOperations { private esClient = ElasticSearch.getESClient(); @@ -98,9 +101,13 @@ class ChallengeDomain extends CoreOperations { id: string = "" ) { let legacyChallengeId: number | null = null; - - if (input.legacy == null || input.legacy.pureV5Task !== true) { - const { track, subTrack, isTask } = legacyMapper.mapTrackAndType(trackId, typeId, tags); + const trackAndTypeMapped = legacyMapper.mapTrackAndType(trackId, typeId, tags); + // Skip creating legacy challenge if + // 1. challenge is a Pure V5 Task, or + // 2. challenge is not a draft, or + // 3. challenge is a draft but track and type can not be mapped - indicating that challenge is not a legacy challenge + if (trackAndTypeMapped != null && (input.legacy == null || input.legacy.pureV5Task !== true)) { + const { track, subTrack, isTask } = trackAndTypeMapped; const directProjectId = input.legacy == null ? 0 : input.legacy.directProjectId; // v5 API can set directProjectId const reviewType = input.legacy == null ? "INTERNAL" : input.legacy.reviewType; // v5 API can set reviewType const confidentialityType = @@ -224,8 +231,8 @@ class ChallengeDomain extends CoreOperations { payments: [], overview: { type: prizeType, - totalPrizes: type === "USD" ? totalPlacementPrizes / 100 : totalPlacementPrizes, - totalPrizesInCents: type === "USD" ? totalPlacementPrizes : undefined, + totalPrizes: prizeType === "USD" ? totalPlacementPrizes / 100 : totalPlacementPrizes, + totalPrizesInCents: prizeType === "USD" ? totalPlacementPrizes : undefined, }, ...input, prizeSets: (input.prizeSets ?? []).map((prizeSet) => { @@ -346,7 +353,7 @@ class ChallengeDomain extends CoreOperations { try { let legacyId: number | null = null; - if (challenge.legacy!.pureV5Task !== true) { + if (!this.isPureV5Challenge(challenge)) { // Begin Anti-Corruption Layer if (input.status === ChallengeStatuses.Draft) { if (items.length === 0 || items[0] == null) { @@ -508,7 +515,7 @@ class ChallengeDomain extends CoreOperations { prizes: (prizeSet.prizes ?? []).map((prize) => { return { ...prize, - value: prizeType == 'USD' ? prize.amountInCents! / 100 : prize.value, + value: prizeType === 'USD' ? prize.amountInCents! / 100 : prize.value, }; }), }; @@ -523,8 +530,8 @@ class ChallengeDomain extends CoreOperations { endDate: input?.status === ChallengeStatuses.Completed ? new Date().toISOString() : (input.endDate ?? undefined), overview: input.overview != null ? { type: prizeType, - totalPrizes: prizeType == 'USD' ? totalPlacementPrize / 100 : totalPlacementPrize, - totalPrizesInCents: prizeType == 'USD' ? totalPlacementPrize: undefined, + totalPrizes: prizeType === 'USD' ? totalPlacementPrize / 100 : totalPlacementPrize, + totalPrizesInCents: prizeType === 'USD' ? totalPlacementPrize: undefined, } : undefined, legacyId: legacyId ?? undefined, constraints: input.constraints ?? undefined, @@ -907,6 +914,13 @@ class ChallengeDomain extends CoreOperations { return totalAmount; } + + private isPureV5Challenge(challenge: Challenge) { + return ( + challenge.legacy?.pureV5Task === true || + PURE_V5_CHALLENGE_TEMPLATE_IDS.includes(challenge.timelineTemplateId ?? "") + ); + } } interface IUpdateDataFromACL { diff --git a/src/util/LegacyMapper.ts b/src/util/LegacyMapper.ts index fcc8960..92c5017 100644 --- a/src/util/LegacyMapper.ts +++ b/src/util/LegacyMapper.ts @@ -89,7 +89,14 @@ class LegacyMapper { }; public mapTrackAndType(trackId: string, typeId: string, tags: string[]) { - return V5_TO_V4[trackId][typeId](tags); + try { + return V5_TO_V4[trackId][typeId](tags); + } catch (e) { + console.log( + `Could not map trackId: ${trackId} and typeId: ${typeId}. Not a legacy challenge` + ); + return null; + } } private mapTrackAndTypeToCategoryStudioSpecAndMmSpec( From 56f75d818e64f382afc61f4cf5bae7ba004a0d61 Mon Sep 17 00:00:00 2001 From: Rakib Ansary Date: Sat, 25 Nov 2023 02:39:46 +0600 Subject: [PATCH 08/12] fix: pure-v5 challenge check Signed-off-by: Rakib Ansary --- src/domain/Challenge.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/domain/Challenge.ts b/src/domain/Challenge.ts index 8dcb449..7af0b8c 100644 --- a/src/domain/Challenge.ts +++ b/src/domain/Challenge.ts @@ -106,7 +106,7 @@ class ChallengeDomain extends CoreOperations { // 1. challenge is a Pure V5 Task, or // 2. challenge is not a draft, or // 3. challenge is a draft but track and type can not be mapped - indicating that challenge is not a legacy challenge - if (trackAndTypeMapped != null && (input.legacy == null || input.legacy.pureV5Task !== true)) { + if (trackAndTypeMapped != null && (input.legacy == null || !this.isPureV5Challenge(input))) { const { track, subTrack, isTask } = trackAndTypeMapped; const directProjectId = input.legacy == null ? 0 : input.legacy.directProjectId; // v5 API can set directProjectId const reviewType = input.legacy == null ? "INTERNAL" : input.legacy.reviewType; // v5 API can set reviewType @@ -915,7 +915,7 @@ class ChallengeDomain extends CoreOperations { return totalAmount; } - private isPureV5Challenge(challenge: Challenge) { + private isPureV5Challenge(challenge: { timelineTemplateId?: string; legacy?: Challenge_Legacy }) { return ( challenge.legacy?.pureV5Task === true || PURE_V5_CHALLENGE_TEMPLATE_IDS.includes(challenge.timelineTemplateId ?? "") From c97179b2c4e2f99dbb388c5aedb220c422fb1af4 Mon Sep 17 00:00:00 2001 From: liuliquan Date: Sat, 25 Nov 2023 22:27:45 +0800 Subject: [PATCH 09/12] refactor: construct data object for update --- src/domain/Challenge.ts | 98 ++++++++++++++++++++--------------------- 1 file changed, 48 insertions(+), 50 deletions(-) diff --git a/src/domain/Challenge.ts b/src/domain/Challenge.ts index 7af0b8c..9ec698b 100644 --- a/src/domain/Challenge.ts +++ b/src/domain/Challenge.ts @@ -488,56 +488,54 @@ class ChallengeDomain extends CoreOperations { } } - updatedChallenge = await super.update( - scanCriteria, - // prettier-ignore - { - name: input.name != null ? sanitize(input.name) : undefined, - typeId: input.typeId != null ? input.typeId : undefined, - trackId: input.trackId != null ? input.trackId : undefined, - timelineTemplateId: input.timelineTemplateId != null ? input.timelineTemplateId : undefined, - legacy: input.legacy != null ? input.legacy : undefined, - billing: input.billing != null ? input.billing : undefined, - description: input.description != null ? sanitize(input.description, input.descriptionFormat ?? challenge.descriptionFormat) : undefined, - privateDescription: input.privateDescription != null ? sanitize(input.privateDescription, input.descriptionFormat ?? challenge.descriptionFormat) : undefined, - descriptionFormat: input.descriptionFormat != null ? input.descriptionFormat : undefined, - task: input.task != null ? input.task : undefined, - winners: input.winnerUpdate != null ? input.winnerUpdate.winners : undefined, - payments: input.paymentUpdate != null ? input.paymentUpdate.payments : undefined, - discussions: input.discussionUpdate != null ? input.discussionUpdate.discussions : undefined, - metadata: input.metadataUpdate != null ? input.metadataUpdate.metadata : undefined, - phases: input.phaseUpdate != null ? input.phaseUpdate.phases : undefined, - events: input.eventUpdate != null ? input.eventUpdate.events : undefined, - terms: input.termUpdate != null ? input.termUpdate.terms : undefined, - prizeSets: input.prizeSetUpdate != null ? input.prizeSetUpdate.prizeSets.map((prizeSet) => { - return { - ...prizeSet, - prizes: (prizeSet.prizes ?? []).map((prize) => { - return { - ...prize, - value: prizeType === 'USD' ? prize.amountInCents! / 100 : prize.value, - }; - }), - }; - }) : undefined, - tags: input.tagUpdate != null ? input.tagUpdate.tags : undefined, - skills: input.skillUpdate != null ? input.skillUpdate.skills : undefined, - status: input.status ?? undefined, - attachments: input.attachmentUpdate != null ? input.attachmentUpdate.attachments : undefined, - groups: input.groupUpdate != null ? input.groupUpdate.groups : undefined, - projectId: input.projectId ?? undefined, - startDate: input.startDate ?? undefined, - endDate: input?.status === ChallengeStatuses.Completed ? new Date().toISOString() : (input.endDate ?? undefined), - overview: input.overview != null ? { - type: prizeType, - totalPrizes: prizeType === 'USD' ? totalPlacementPrize / 100 : totalPlacementPrize, - totalPrizesInCents: prizeType === 'USD' ? totalPlacementPrize: undefined, - } : undefined, - legacyId: legacyId ?? undefined, - constraints: input.constraints ?? undefined, - }, - metadata - ); + // prettier-ignore + const dataToUpdate = { + name: input.name != null ? sanitize(input.name) : undefined, + typeId: input.typeId != null ? input.typeId : undefined, + trackId: input.trackId != null ? input.trackId : undefined, + timelineTemplateId: input.timelineTemplateId != null ? input.timelineTemplateId : undefined, + legacy: input.legacy != null ? input.legacy : undefined, + billing: input.billing != null ? input.billing : undefined, + description: input.description != null ? sanitize(input.description, input.descriptionFormat ?? challenge.descriptionFormat) : undefined, + privateDescription: input.privateDescription != null ? sanitize(input.privateDescription, input.descriptionFormat ?? challenge.descriptionFormat) : undefined, + descriptionFormat: input.descriptionFormat != null ? input.descriptionFormat : undefined, + task: input.task != null ? input.task : undefined, + winners: input.winnerUpdate != null ? input.winnerUpdate.winners : undefined, + payments: input.paymentUpdate != null ? input.paymentUpdate.payments : undefined, + discussions: input.discussionUpdate != null ? input.discussionUpdate.discussions : undefined, + metadata: input.metadataUpdate != null ? input.metadataUpdate.metadata : undefined, + phases: input.phaseUpdate != null ? input.phaseUpdate.phases : undefined, + events: input.eventUpdate != null ? input.eventUpdate.events : undefined, + terms: input.termUpdate != null ? input.termUpdate.terms : undefined, + prizeSets: input.prizeSetUpdate != null ? input.prizeSetUpdate.prizeSets.map((prizeSet) => { + return { + ...prizeSet, + prizes: (prizeSet.prizes ?? []).map((prize) => { + return { + ...prize, + value: prizeType === 'USD' ? prize.amountInCents! / 100 : prize.value, + }; + }), + }; + }) : undefined, + tags: input.tagUpdate != null ? input.tagUpdate.tags : undefined, + skills: input.skillUpdate != null ? input.skillUpdate.skills : undefined, + status: input.status ?? undefined, + attachments: input.attachmentUpdate != null ? input.attachmentUpdate.attachments : undefined, + groups: input.groupUpdate != null ? input.groupUpdate.groups : undefined, + projectId: input.projectId ?? undefined, + startDate: input.startDate ?? undefined, + endDate: input?.status === ChallengeStatuses.Completed ? new Date().toISOString() : (input.endDate ?? undefined), + overview: input.overview != null ? { + type: prizeType, + totalPrizes: prizeType === 'USD' ? totalPlacementPrize / 100 : totalPlacementPrize, + totalPrizesInCents: prizeType === 'USD' ? totalPlacementPrize: undefined, + } : undefined, + legacyId: legacyId ?? undefined, + constraints: input.constraints ?? undefined, + }; + + updatedChallenge = await super.update(scanCriteria, dataToUpdate, metadata); } catch (err) { if (baValidation != null) { await lockConsumeAmount(baValidation, true); From 9428949422ba2c9df6c290f42254ad74a65f2c5c Mon Sep 17 00:00:00 2001 From: liuliquan Date: Sat, 25 Nov 2023 22:31:23 +0800 Subject: [PATCH 10/12] CORE-63 Post Challenge events to Harmony after CRUD --- src/domain/Challenge.ts | 7 +++++++ src/helpers/Harmony.ts | 44 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 src/helpers/Harmony.ts diff --git a/src/domain/Challenge.ts b/src/domain/Challenge.ts index 9ec698b..87797f3 100644 --- a/src/domain/Challenge.ts +++ b/src/domain/Challenge.ts @@ -43,6 +43,7 @@ import { V5_TRACK_IDS_TO_NAMES, V5_TYPE_IDS_TO_NAMES } from "../common/Conversio import PaymentCreator, { PaymentDetail } from "../util/PaymentCreator"; import { getChallengeResources } from "../api/v5Api"; import m2mToken from "../helpers/MachineToMachineToken"; +import { sendHarmonyEvent } from "../helpers/Harmony"; if (!process.env.GRPC_ACL_SERVER_HOST || !process.env.GRPC_ACL_SERVER_PORT) { throw new Error("Missing required configurations GRPC_ACL_SERVER_HOST and GRPC_ACL_SERVER_PORT"); @@ -267,6 +268,7 @@ class ChallengeDomain extends CoreOperations { }; newChallenge = await super.create(challenge, metadata); + await sendHarmonyEvent("CREATE", "Challenge", newChallenge); } catch (err) { // Rollback lock amount if (baValidation != null) { @@ -536,6 +538,8 @@ class ChallengeDomain extends CoreOperations { }; updatedChallenge = await super.update(scanCriteria, dataToUpdate, metadata); + + await sendHarmonyEvent("UPDATE", "Challenge", { ...dataToUpdate, id: challenge.id }); } catch (err) { if (baValidation != null) { await lockConsumeAmount(baValidation, true); @@ -725,12 +729,14 @@ class ChallengeDomain extends CoreOperations { if (baValidation != null) await lockConsumeAmount(baValidation); try { await super.update(scanCriteria, dynamoUpdate); + await sendHarmonyEvent("UPDATE", "Challenge", { ...data, id }); } catch (err) { if (baValidation != null) await lockConsumeAmount(baValidation, true); throw err; } } else { await super.update(scanCriteria, dynamoUpdate); + await sendHarmonyEvent("UPDATE", "Challenge", { ...data, id }); console.log("Challenge Completed"); const completedChallenge = await this.lookup(DomainHelper.getLookupCriteria("id", id)); @@ -805,6 +811,7 @@ class ChallengeDomain extends CoreOperations { try { const result = await super.delete(lookupCriteria); + await sendHarmonyEvent("DELETE", "Challenge", { id: challenge.id }); return result; } catch (err) { await lockConsumeAmount(baValidation, true); diff --git a/src/helpers/Harmony.ts b/src/helpers/Harmony.ts new file mode 100644 index 0000000..9fb363f --- /dev/null +++ b/src/helpers/Harmony.ts @@ -0,0 +1,44 @@ +import _ from "lodash"; +import { InvokeCommand, LambdaClient } from "@aws-sdk/client-lambda"; + +import { EVENT_ORIGINATOR } from "../common/Constants"; + +const FunctionName = + process.env.HARMONY_LAMBDA_FUNCTION ?? + "arn:aws:lambda:us-east-1:811668436784:function:harmony-api-dev-processMessage"; + +const harmonyClient = new LambdaClient({ region: process.env.AWS_REGION, maxAttempts: 2 }); + +/** + * Send event to Harmony. + * @param eventType The event type + * @param payloadType The payload type + * @param payload The event payload + */ +export async function sendHarmonyEvent(eventType: string, payloadType: string, payload: object) { + const event = { + publisher: EVENT_ORIGINATOR, + timestamp: new Date().getTime(), + eventType, + payloadType, + payload, + }; + + const invokeCommand = new InvokeCommand({ + FunctionName, + InvocationType: "RequestResponse", + Payload: JSON.stringify(event), + LogType: "None" + }); + + const result = await harmonyClient.send(invokeCommand); + + if (result.FunctionError) { + console.error( + "Failed to send Harmony event", + result.FunctionError, + result.Payload?.transformToString() + ); + throw new Error(result.FunctionError); + } +} From cf51c49c23ba0b3db709636e7715bc30bdc96016 Mon Sep 17 00:00:00 2001 From: liuliquan Date: Mon, 4 Dec 2023 07:04:17 +0800 Subject: [PATCH 11/12] feat: send billingAccountId --- src/domain/Challenge.ts | 10 +++++----- src/helpers/Harmony.ts | 4 +++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/domain/Challenge.ts b/src/domain/Challenge.ts index f03f0aa..f1f4a9f 100644 --- a/src/domain/Challenge.ts +++ b/src/domain/Challenge.ts @@ -268,7 +268,7 @@ class ChallengeDomain extends CoreOperations { }; newChallenge = await super.create(challenge, metadata); - await sendHarmonyEvent("CREATE", "Challenge", newChallenge); + await sendHarmonyEvent("CREATE", "Challenge", newChallenge, input.billing?.billingAccountId!); } catch (err) { // Rollback lock amount if (baValidation != null) { @@ -539,7 +539,7 @@ class ChallengeDomain extends CoreOperations { updatedChallenge = await super.update(scanCriteria, dataToUpdate, metadata); - await sendHarmonyEvent("UPDATE", "Challenge", { ...dataToUpdate, id: challenge.id }); + await sendHarmonyEvent("UPDATE", "Challenge", { ...dataToUpdate, id: challenge.id }, input.billing?.billingAccountId ?? challenge?.billing?.billingAccountId); } catch (err) { if (baValidation != null) { await lockConsumeAmount(baValidation, true); @@ -729,14 +729,14 @@ class ChallengeDomain extends CoreOperations { if (baValidation != null) await lockConsumeAmount(baValidation); try { await super.update(scanCriteria, dynamoUpdate); - await sendHarmonyEvent("UPDATE", "Challenge", { ...data, id }); + await sendHarmonyEvent("UPDATE", "Challenge", { ...data, id }, challenge.billing?.billingAccountId); } catch (err) { if (baValidation != null) await lockConsumeAmount(baValidation, true); throw err; } } else { await super.update(scanCriteria, dynamoUpdate); - await sendHarmonyEvent("UPDATE", "Challenge", { ...data, id }); + await sendHarmonyEvent("UPDATE", "Challenge", { ...data, id }, challenge.billing?.billingAccountId); console.log("Challenge Completed"); const completedChallenge = await this.lookup(DomainHelper.getLookupCriteria("id", id)); @@ -811,7 +811,7 @@ class ChallengeDomain extends CoreOperations { try { const result = await super.delete(lookupCriteria); - await sendHarmonyEvent("DELETE", "Challenge", { id: challenge.id }); + await sendHarmonyEvent("DELETE", "Challenge", { id: challenge.id }, challenge.billing?.billingAccountId); return result; } catch (err) { await lockConsumeAmount(baValidation, true); diff --git a/src/helpers/Harmony.ts b/src/helpers/Harmony.ts index 9fb363f..ebf9a2c 100644 --- a/src/helpers/Harmony.ts +++ b/src/helpers/Harmony.ts @@ -14,14 +14,16 @@ const harmonyClient = new LambdaClient({ region: process.env.AWS_REGION, maxAtte * @param eventType The event type * @param payloadType The payload type * @param payload The event payload + * @param billingAccountId The billing account id */ -export async function sendHarmonyEvent(eventType: string, payloadType: string, payload: object) { +export async function sendHarmonyEvent(eventType: string, payloadType: string, payload: object, billingAccountId?: number) { const event = { publisher: EVENT_ORIGINATOR, timestamp: new Date().getTime(), eventType, payloadType, payload, + billingAccountId, }; const invokeCommand = new InvokeCommand({ From 5f75010e85b84f28ab1e5b403f6eb989815955d4 Mon Sep 17 00:00:00 2001 From: liuliquan Date: Tue, 5 Dec 2023 05:20:55 +0800 Subject: [PATCH 12/12] fix: send CREATE event when billing account id is set --- src/domain/Challenge.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/domain/Challenge.ts b/src/domain/Challenge.ts index f1f4a9f..2fbd82c 100644 --- a/src/domain/Challenge.ts +++ b/src/domain/Challenge.ts @@ -539,7 +539,18 @@ class ChallengeDomain extends CoreOperations { updatedChallenge = await super.update(scanCriteria, dataToUpdate, metadata); - await sendHarmonyEvent("UPDATE", "Challenge", { ...dataToUpdate, id: challenge.id }, input.billing?.billingAccountId ?? challenge?.billing?.billingAccountId); + const newChallenge = updatedChallenge.items[0]; + if (newChallenge.billing?.billingAccountId !== challenge?.billing?.billingAccountId) { + // For a New/Draft challenge, it might miss billing account id + // However when challenge activates, the billing account id will be provided (challenge-api validates it) + // In such case, send a CREATE event with whole challenge data (it's fine for search-indexer since it upserts for CREATE) + // Otherwise, the outer customer specified by the billing account id (like Topgear) will never receive a challenge CREATE event + await sendHarmonyEvent("CREATE", "Challenge", newChallenge, newChallenge.billing?.billingAccountId); + } else { + // Send only the updated data + // Some field like chanllege description could be big, don't include them if they're not actually updated + await sendHarmonyEvent("UPDATE", "Challenge", { ...dataToUpdate, id: newChallenge.id }, newChallenge.billing?.billingAccountId); + } } catch (err) { if (baValidation != null) { await lockConsumeAmount(baValidation, true);