Skip to content

Commit d1d1984

Browse files
committed
refactor: finalize spend management
1 parent e963e79 commit d1d1984

File tree

21 files changed

+368
-82
lines changed

21 files changed

+368
-82
lines changed

apps/backend/src/build/job.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ async function updateUsage(project: Project) {
5353
return lock.acquire(["updateUsage", account.id], async () => {
5454
const [totalScreenshots, spendLimitThreshold] = await Promise.all([
5555
manager.getCurrentPeriodScreenshots(),
56-
getSpendLimitThreshold(account),
56+
getSpendLimitThreshold({ account, comparePreviousUsage: true }),
5757
]);
5858

5959
await updateStripeUsage({ account, totalScreenshots });

apps/backend/src/database/services/spend-limit.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,16 @@ export type SpendLimitThreshold = (typeof THRESHOLDS)[number];
1010
/**
1111
* Get the spend limit threshold that has been reached for the first time.
1212
*/
13-
export async function getSpendLimitThreshold(
14-
account: Account,
15-
): Promise<SpendLimitThreshold | null> {
13+
export async function getSpendLimitThreshold(input: {
14+
account: Account;
15+
/**
16+
* Take in account the previous usage to compare with the current usage.
17+
* Used to be sure that the threshold has been reached for the first time.
18+
* Not needed when it's trigerred by an action.
19+
*/
20+
comparePreviousUsage: boolean;
21+
}): Promise<SpendLimitThreshold | null> {
22+
const { account, comparePreviousUsage } = input;
1623
const manager = account.$getSubscriptionManager();
1724

1825
if (account.meteredSpendLimitByPeriod === null) {
@@ -21,7 +28,9 @@ export async function getSpendLimitThreshold(
2128

2229
const [currentCost, previousUsageCost] = await Promise.all([
2330
manager.getAdditionalScreenshotCost(),
24-
manager.getAdditionalScreenshotCost({ to: "previousUsage" }),
31+
comparePreviousUsage
32+
? manager.getAdditionalScreenshotCost({ to: "previousUsage" })
33+
: null,
2534
]);
2635

2736
const spendLimit = account.meteredSpendLimitByPeriod;
@@ -30,7 +39,7 @@ export async function getSpendLimitThreshold(
3039
if (
3140
// The highest threshold is reached.
3241
(acc === null || acc < threshold) &&
33-
previousUsageCost <= limitAtThreshold &&
42+
(previousUsageCost === null || previousUsageCost <= limitAtThreshold) &&
3443
currentCost > limitAtThreshold
3544
) {
3645
return threshold;

apps/backend/src/database/services/spent-limit.e2e.test.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,14 +132,20 @@ describe("spent limit", () => {
132132
});
133133

134134
it("returns null", async () => {
135-
const threshold = await getSpendLimitThreshold(account);
135+
const threshold = await getSpendLimitThreshold({
136+
account,
137+
comparePreviousUsage: true,
138+
});
136139
expect(threshold).toBeNull();
137140
});
138141
});
139142

140143
describe("with spend limit", () => {
141144
it("returns the correct threshold", async () => {
142-
const threshold = await getSpendLimitThreshold(account);
145+
const threshold = await getSpendLimitThreshold({
146+
account,
147+
comparePreviousUsage: true,
148+
});
143149
expect(threshold).toBe(50);
144150
});
145151
});
@@ -152,7 +158,10 @@ describe("spent limit", () => {
152158
});
153159

154160
it("returns the correct threshold", async () => {
155-
const threshold = await getSpendLimitThreshold(account);
161+
const threshold = await getSpendLimitThreshold({
162+
account,
163+
comparePreviousUsage: true,
164+
});
156165
expect(threshold).toBe(50);
157166
});
158167
});
@@ -165,8 +174,17 @@ describe("spent limit", () => {
165174
});
166175

167176
it("returns null", async () => {
168-
const threshold = await getSpendLimitThreshold(account);
177+
const threshold = await getSpendLimitThreshold({
178+
account,
179+
comparePreviousUsage: true,
180+
});
169181
expect(threshold).toBeNull();
182+
183+
const thresholdWihoutPreviousUsage = await getSpendLimitThreshold({
184+
account,
185+
comparePreviousUsage: false,
186+
});
187+
expect(thresholdWihoutPreviousUsage).toBe(50);
170188
});
171189
});
172190

@@ -183,7 +201,10 @@ describe("spent limit", () => {
183201
});
184202

185203
it("returns the highest threshold reached", async () => {
186-
const threshold = await getSpendLimitThreshold(account);
204+
const threshold = await getSpendLimitThreshold({
205+
account,
206+
comparePreviousUsage: true,
207+
});
187208
expect(threshold).toBe(100);
188209
});
189210
});

apps/backend/src/graphql/definitions/Account.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ import {
1414
TeamUser,
1515
} from "@/database/models/index.js";
1616
import { checkAccountSlug } from "@/database/services/account.js";
17+
import { getSpendLimitThreshold } from "@/database/services/spend-limit.js";
1718
import { getGitlabClient, getGitlabClientFromAccount } from "@/gitlab/index.js";
1819
import {
1920
getAccountBuildMetrics,
2021
getAccountScreenshotMetrics,
2122
} from "@/metrics/account.js";
23+
import { sendNotification } from "@/notification/index.js";
2224
import { uninstallSlackInstallation } from "@/slack/index.js";
2325
import { encodeStripeClientReferenceId } from "@/stripe/index.js";
2426

@@ -534,7 +536,54 @@ export const resolvers: IResolvers = {
534536
}
535537
}
536538

537-
return account.$query().patchAndFetch(data);
539+
const previousAccount = account.$clone();
540+
await account.$query().patchAndFetch(data);
541+
542+
// If the spend limit has been updated, we may need to notify.
543+
if (
544+
input.meteredSpendLimitByPeriod !== undefined &&
545+
input.meteredSpendLimitByPeriod !== null &&
546+
previousAccount.meteredSpendLimitByPeriod !==
547+
input.meteredSpendLimitByPeriod
548+
) {
549+
await (async () => {
550+
const [threshold, previousThreshold] = await Promise.all([
551+
getSpendLimitThreshold({
552+
account,
553+
comparePreviousUsage: false,
554+
}),
555+
getSpendLimitThreshold({
556+
account: previousAccount,
557+
comparePreviousUsage: false,
558+
}),
559+
]);
560+
561+
console.log({ threshold, previousThreshold });
562+
563+
// If there is threshold, we don't need to notify the user.
564+
if (!threshold) {
565+
return;
566+
}
567+
568+
// If it's the same threshold, we don't need to notify the user.
569+
if (threshold === previousThreshold) {
570+
return;
571+
}
572+
573+
const owners = await account.$getOwnerIds();
574+
await sendNotification({
575+
type: "spend_limit",
576+
data: {
577+
accountName: account.name,
578+
accountSlug: account.slug,
579+
threshold,
580+
},
581+
recipients: owners,
582+
});
583+
})();
584+
}
585+
586+
return account;
538587
},
539588
uninstallSlack: async (_root, args, ctx) => {
540589
const { accountId } = args.input;

apps/backend/src/notification/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ export async function sendNotification<
2727
*/
2828
recipients: string[];
2929
}) {
30+
if (input.recipients.length === 0) {
31+
throw new Error("No recipients provided");
32+
}
3033
const workflow = await transaction(async (trx) => {
3134
const workflow = await NotificationWorkflow.query(trx).insertAndFetch({
3235
type: input.type,

apps/frontend/src/containers/Project/Delete.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ function DeleteProjectButton(props: DeleteProjectButtonProps) {
8585
return true;
8686
},
8787
})}
88+
autoFocus
8889
className="mb-4"
8990
label={
9091
<>

apps/frontend/src/containers/Project/Token.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ function RegenerateTokenDialog(props: {
178178
return true;
179179
},
180180
})}
181+
autoFocus
181182
className="mb-4"
182183
label={
183184
<>

apps/frontend/src/containers/Team/Delete.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ function TeamDeleteDialog(props: DeleteTeamButtonProps) {
122122
},
123123
})}
124124
className="mb-4"
125+
autoFocus
125126
label={
126127
<>
127128
Enter the team name <strong>{props.teamSlug}</strong> to

0 commit comments

Comments
 (0)