Skip to content

Commit c41abe7

Browse files
liuliquanrakibansary
liuliquan
andauthored
PLAT-3221 lock/consume budget (#57)
Co-authored-by: Rakib Ansary <rakibansary@topcoder.com>
1 parent 29b4fec commit c41abe7

File tree

2 files changed

+400
-171
lines changed

2 files changed

+400
-171
lines changed

src/api/BillingAccount.ts

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import _ from "lodash";
2+
import axios from "axios";
3+
import { StatusBuilder } from "@grpc/grpc-js";
4+
import { Status } from "@grpc/grpc-js/build/src/constants";
5+
6+
import m2m from "../helpers/MachineToMachineToken";
7+
import { ChallengeStatuses, TGBillingAccounts } from "../common/Constants";
8+
9+
const { V3_BA_API_URL = "https://api.topcoder-dev.com/v3/billing-accounts" } = process.env;
10+
11+
async function lockAmount(billingAccountId: number, dto: LockAmountDTO) {
12+
console.log("BA validation lock amount:", billingAccountId, dto);
13+
14+
try {
15+
const m2mToken = await m2m.getM2MToken();
16+
17+
await axios.patch(
18+
`${V3_BA_API_URL}/${billingAccountId}/lock-amount`,
19+
{
20+
param: dto,
21+
},
22+
{
23+
headers: {
24+
Authorization: `Bearer ${m2mToken}`,
25+
"Content-Type": "application/json",
26+
},
27+
}
28+
);
29+
} catch (err: any) {
30+
throw new StatusBuilder()
31+
.withCode(Status.INTERNAL)
32+
.withDetails(err.response?.data?.result?.content ?? "Failed to lock challenge amount")
33+
.build();
34+
}
35+
}
36+
37+
async function consumeAmount(billingAccountId: number, dto: ConsumeAmountDTO) {
38+
console.log("BA validation consume amount:", billingAccountId, dto);
39+
40+
try {
41+
const m2mToken = await m2m.getM2MToken();
42+
43+
await axios.patch(
44+
`${V3_BA_API_URL}/${billingAccountId}/consume-amount`,
45+
{
46+
param: dto,
47+
},
48+
{
49+
headers: {
50+
Authorization: `Bearer ${m2mToken}`,
51+
"Content-Type": "application/json",
52+
},
53+
}
54+
);
55+
} catch (err: any) {
56+
throw new StatusBuilder()
57+
.withCode(Status.INTERNAL)
58+
.withDetails(err.response?.data?.result?.content ?? "Failed to consume challenge amount")
59+
.build();
60+
}
61+
}
62+
63+
interface LockAmountDTO {
64+
challengeId: string;
65+
lockAmount: number;
66+
}
67+
interface ConsumeAmountDTO {
68+
challengeId: string;
69+
consumeAmount: number;
70+
markup?: number;
71+
}
72+
73+
// prettier-ignore
74+
export async function lockConsumeAmount(baValidation: BAValidation, rollback: boolean = false): Promise<void> {
75+
if (!_.isNumber(baValidation.billingAccountId)) {
76+
console.warn("Challenge doesn't have billing account id:", baValidation);
77+
return;
78+
}
79+
if (_.includes(TGBillingAccounts, baValidation.billingAccountId)) {
80+
console.info("Ignore BA validation for Topgear account:", baValidation.billingAccountId);
81+
return;
82+
}
83+
84+
console.log("BA validation:", baValidation);
85+
86+
if (
87+
baValidation.status === ChallengeStatuses.New ||
88+
baValidation.status === ChallengeStatuses.Draft ||
89+
baValidation.status === ChallengeStatuses.Active ||
90+
baValidation.status === ChallengeStatuses.Approved
91+
) {
92+
// Update lock amount
93+
const currAmount = baValidation.totalPrizesInCents / 100;
94+
const prevAmount = baValidation.prevTotalPrizesInCents / 100;
95+
96+
if (currAmount !== prevAmount) {
97+
await lockAmount(baValidation.billingAccountId, {
98+
challengeId: baValidation.challengeId!,
99+
lockAmount: rollback ? prevAmount : currAmount,
100+
});
101+
}
102+
} else if (baValidation.status === ChallengeStatuses.Completed) {
103+
// Note an already completed challenge could still be updated with prizes
104+
const currAmount = baValidation.totalPrizesInCents / 100;
105+
const prevAmount = baValidation.prevStatus === ChallengeStatuses.Completed ? baValidation.prevTotalPrizesInCents / 100 : 0;
106+
107+
if (currAmount !== prevAmount) {
108+
await consumeAmount(baValidation.billingAccountId, {
109+
challengeId: baValidation.challengeId!,
110+
consumeAmount: rollback ? prevAmount : currAmount,
111+
markup: baValidation.markup,
112+
});
113+
}
114+
} else if (
115+
baValidation.status === ChallengeStatuses.Deleted ||
116+
baValidation.status === ChallengeStatuses.Canceled ||
117+
baValidation.status === ChallengeStatuses.CancelledFailedReview ||
118+
baValidation.status === ChallengeStatuses.CancelledFailedScreening ||
119+
baValidation.status === ChallengeStatuses.CancelledZeroSubmissions ||
120+
baValidation.status === ChallengeStatuses.CancelledWinnerUnresponsive ||
121+
baValidation.status === ChallengeStatuses.CancelledClientRequest ||
122+
baValidation.status === ChallengeStatuses.CancelledRequirementsInfeasible ||
123+
baValidation.status === ChallengeStatuses.CancelledZeroRegistrations ||
124+
baValidation.status === ChallengeStatuses.CancelledPaymentFailed
125+
) {
126+
// Challenge canceled, unlock previous locked amount
127+
const currAmount = 0;
128+
const prevAmount = baValidation.prevTotalPrizesInCents / 100;
129+
130+
if (currAmount !== prevAmount) {
131+
await lockAmount(baValidation.billingAccountId, {
132+
challengeId: baValidation.challengeId!,
133+
lockAmount: rollback ? prevAmount : 0,
134+
});
135+
}
136+
}
137+
}
138+
139+
export interface BAValidation {
140+
challengeId?: string;
141+
billingAccountId?: number;
142+
markup?: number;
143+
prevStatus?: string;
144+
status?: string;
145+
prevTotalPrizesInCents: number;
146+
totalPrizesInCents: number;
147+
}

0 commit comments

Comments
 (0)