Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RW-13480][risk=no] Workspace Banners for Initial Credits #8951

Open
wants to merge 47 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
40bdb61
Added basic tests.
evrii Nov 12, 2024
cc7918c
Add expiration date.
evrii Nov 12, 2024
dee730b
Update styling
evrii Nov 12, 2024
e76615e
Add checks for extension button.
evrii Nov 12, 2024
a5ff2f3
Add extension and info buttons.
evrii Nov 12, 2024
0adb7e4
Merge remote-tracking branch 'origin/main' into eric/RW-13481
evrii Nov 14, 2024
ad412f5
Merge remote-tracking branch 'origin/main' into eric/RW-13481
evrii Nov 15, 2024
c214129
Added extension field.
evrii Nov 18, 2024
cd7f7d3
First pass at banners
evrii Nov 18, 2024
bdd7f49
Simplified logic?
evrii Nov 18, 2024
a2dbc9d
Added config values.
evrii Nov 18, 2024
42b159c
[RW-13735][risk=no] Added modal to trigger initial credit extension. …
evrii Nov 19, 2024
f50fb7b
Merge remote-tracking branch 'origin/eric/RW-13481' into eric/RW-13480
evrii Nov 19, 2024
bd12bb9
Fix title and eligibility usage
evrii Nov 19, 2024
7b35bd6
Merge remote-tracking branch 'origin/main' into eric/RW-13480
evrii Nov 19, 2024
6480a2f
[risk=low][no ticket] [dependabot] Bump cross-spawn from 7.0.3 to 7.0…
dependabot[bot] Nov 19, 2024
7881475
laucn multi-zone (#8956)
yonghaoy Nov 19, 2024
92a2784
remove getUploadResult in verifyAndLog (#8955)
Qi77Qi Nov 19, 2024
0abe313
Merge remote-tracking branch 'origin/main' into eric/RW-13480
evrii Nov 19, 2024
0f0326e
Update banner
evrii Nov 19, 2024
8092dba
Cleaner banner?
evrii Nov 19, 2024
e507305
Merge remote-tracking branch 'origin/main' into eric/RW-13480
evrii Nov 20, 2024
d7ec332
Cleanup
evrii Nov 20, 2024
34957ec
Add expiring soon banners.
evrii Nov 20, 2024
d1b1f87
Add expired but eligible tests
evrii Nov 20, 2024
f463eec
Add expired and ineligible tests
evrii Nov 20, 2024
73378ea
Hide banner if not appropriate
evrii Nov 20, 2024
3c286c7
Cleanup
evrii Nov 22, 2024
cbca190
Cleanup
evrii Nov 22, 2024
cd304ee
Updated language
evrii Nov 22, 2024
300240f
Updated spacing
evrii Nov 22, 2024
3c6ec5e
Updated test email.
evrii Nov 22, 2024
d6364e3
Merge main
evrii Dec 3, 2024
8f15eb8
Made variables explicit
evrii Dec 3, 2024
2e317b1
Cleanup
evrii Dec 3, 2024
b96ee0e
Changed directory
evrii Dec 3, 2024
6d30024
Swap function
evrii Dec 3, 2024
7b352b1
Fix link
evrii Dec 3, 2024
d66c840
Added missing extension logic
evrii Dec 3, 2024
62b3414
Cleanup
evrii Dec 3, 2024
df7e585
Merge main
evrii Dec 3, 2024
fa78083
Fix name of mapper
evrii Dec 3, 2024
db9d522
Add missing condition
evrii Dec 3, 2024
0954fd0
Cleanup
evrii Dec 3, 2024
ef63dde
Add comment
evrii Dec 3, 2024
db7a2ae
Removed unused enum
evrii Dec 3, 2024
2cc9dfa
Cleanup
evrii Dec 3, 2024
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 @@ -280,7 +280,7 @@ public void checkCreditsExpirationForUserIDs(List<Long> userIdsList) {
*
* @param user - The user whose initial credits expiration time is being checked
* @return The expiration time of the user's initial credits, if they have a
* UserInitialCreditsExpiration record and they have not been bypassed personally or
* UserInitialCreditsExpiration record, and they have not been bypassed personally or
* institutionally.
*/
public Optional<Timestamp> getCreditsExpiration(DbUser user) {
Expand All @@ -290,6 +290,21 @@ public Optional<Timestamp> getCreditsExpiration(DbUser user) {
.map(DbUserInitialCreditsExpiration::getExpirationTime);
}

/**
* For the given user, check when the user's initial credits were extended, if relevant.
*
* @param user - The user whose initial credits extension time is being checked
* @return The extension time of the user's initial credits, if they have a
* UserInitialCreditsExpiration record, and they have not been bypassed personally or
* institutionally.
*/
public Optional<Timestamp> getCreditsExtension(DbUser user) {
return Optional.ofNullable(user.getUserInitialCreditsExpiration())
.filter(exp -> !exp.isBypassed()) // If the expiration is bypassed, return empty.
.filter(exp -> !institutionService.shouldBypassForCreditsExpiration(user))
.map(DbUserInitialCreditsExpiration::getExtensionTime);
}

/**
* Check if the user's initial credits have expired.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,13 @@ public Long getInitialCreditsExpiration(
return initialCreditsService.getCreditsExpiration(source).map(this::timestamp).orElse(null);
}

@Named("getInitialCreditsExtension")
@Nullable
public Long getInitialCreditsExtension(
DbUser source, @Context InitialCreditsService initialCreditsService) {
return initialCreditsService.getCreditsExtension(source).map(this::timestamp).orElse(null);
}

@Named("getBillingStatus")
public BillingStatus getBillingStatus(DbWorkspace dbWorkspace) {
return (isInitialCredits(dbWorkspace.getBillingAccountName(), workbenchConfigProvider.get())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ public interface WorkspaceMapper {
target = "initialCredits.expirationEpochMillis",
source = "dbWorkspace.creator",
qualifiedByName = "getInitialCreditsExpiration")
@Mapping(
target = "initialCredits.extensionEpochMillis",
source = "dbWorkspace.creator",
qualifiedByName = "getInitialCreditsExtension")
@Mapping(target = "cdrVersionId", source = "dbWorkspace.cdrVersion")
@Mapping(target = "accessTierShortName", source = "dbWorkspace.cdrVersion.accessTier.shortName")
@Mapping(target = "googleProject", source = "dbWorkspace.googleProject")
Expand Down Expand Up @@ -130,6 +134,10 @@ default List<WorkspaceResponse> toApiWorkspaceResponseList(
target = "initialCredits.expirationEpochMillis",
source = "creator",
qualifiedByName = "getInitialCreditsExpiration")
@Mapping(
target = "initialCredits.extensionEpochMillis",
source = "creator",
qualifiedByName = "getInitialCreditsExpiration")
@Mapping(target = "initialCredits.exhausted", source = "dbWorkspace.initialCreditsExhausted")
@Mapping(target = "etag", source = "version", qualifiedByName = "versionToEtag")
@Mapping(
Expand Down
4 changes: 3 additions & 1 deletion api/src/main/resources/workbench-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2326,7 +2326,6 @@ paths:
description: The egress event was successfully handled.
content: {}
x-codegen-request-body-name: request

/v1/admin/users/{userId}/egressBypassWindow:
get:
tags:
Expand Down Expand Up @@ -13145,6 +13144,9 @@ components:
expirationEpochMillis:
type: integer
format: int64
extensionEpochMillis:
type: integer
format: int64
VwbEgressEventRequest:
type: object
properties:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -861,6 +861,7 @@ public void test_deletedWorkspaceUsageIsConsidered_whenAnotherWorkspaceExceedsLi
public void test_none() {
DbUser user = new DbUser();
assertThat(initialCreditsService.getCreditsExpiration(user)).isEmpty();
assertThat(initialCreditsService.getCreditsExtension(user)).isEmpty();
}

@Test
Expand All @@ -870,6 +871,7 @@ public void test_userBypassed() {
.setUserInitialCreditsExpiration(
new DbUserInitialCreditsExpiration().setBypassed(true).setExpirationTime(NOW));
assertThat(initialCreditsService.getCreditsExpiration(user)).isEmpty();
assertThat(initialCreditsService.getCreditsExtension(user)).isEmpty();
}

@Test
Expand All @@ -883,10 +885,11 @@ public void test_institutionBypassed() {
.setExpirationTime(NOW)));
when(institutionService.shouldBypassForCreditsExpiration(user)).thenReturn(true);
assertThat(initialCreditsService.getCreditsExpiration(user)).isEmpty();
assertThat(initialCreditsService.getCreditsExtension(user)).isEmpty();
}

@Test
public void test_nullTimestamp() {
public void test_nullExpirationTimestamp() {
DbUser user =
new DbUser()
.setUserInitialCreditsExpiration(
Expand All @@ -895,14 +898,32 @@ public void test_nullTimestamp() {
}

@Test
public void test_validTimestamp() {
public void test_nullExtensionTimestamp() {
DbUser user =
new DbUser()
.setUserInitialCreditsExpiration(
new DbUserInitialCreditsExpiration().setBypassed(false).setExtensionTime(null));
assertThat(initialCreditsService.getCreditsExtension(user)).isEmpty();
}

@Test
public void test_validExpirationTimestamp() {
DbUser user =
new DbUser()
.setUserInitialCreditsExpiration(
new DbUserInitialCreditsExpiration().setBypassed(false).setExpirationTime(NOW));
assertThat(initialCreditsService.getCreditsExpiration(user)).hasValue(NOW);
}

@Test
public void test_validExtensionTimestamp() {
DbUser user =
new DbUser()
.setUserInitialCreditsExpiration(
new DbUserInitialCreditsExpiration().setBypassed(false).setExtensionTime(NOW));
assertThat(initialCreditsService.getCreditsExtension(user)).hasValue(NOW);
}

@Test
public void test_checkCreditsExpirationForUserIDs_null() {
initialCreditsService.checkCreditsExpirationForUserIDs(null);
Expand Down
206 changes: 206 additions & 0 deletions ui/src/app/pages/workspace/invalid-billing-banner.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import '@testing-library/jest-dom';

import * as React from 'react';
import { MemoryRouter } from 'react-router-dom';

import { ProfileApi } from 'generated/fetch';

import { screen } from '@testing-library/dom';
import { render } from '@testing-library/react';
import { InvalidBillingBanner } from 'app/pages/workspace/invalid-billing-banner';
import { registerApiClient } from 'app/services/swagger-fetch-clients';
import { plusDays } from 'app/utils/dates';
import { currentWorkspaceStore } from 'app/utils/navigation';
import { profileStore, serverConfigStore } from 'app/utils/stores';

import defaultServerConfig from 'testing/default-server-config';
import {
ProfileApiStub,
ProfileStubVariables,
} from 'testing/stubs/profile-api-stub';
import { workspaceDataStub } from 'testing/stubs/workspaces';

describe('InvalidBillingBanner', () => {
const load = jest.fn();
const reload = jest.fn();
const updateCache = jest.fn();
const warningThresholdDays = 5; // arbitrary
const me = ProfileStubVariables.PROFILE_STUB.username;
const someOneElse = 'someOneElse@fake-research-aou.org';

const component = () =>
render(
<MemoryRouter>
<InvalidBillingBanner />
</MemoryRouter>
);

beforeEach(() => {
registerApiClient(ProfileApi, new ProfileApiStub());

profileStore.set({
profile: ProfileStubVariables.PROFILE_STUB,
load,
reload,
updateCache,
});
serverConfigStore.set({
config: {
...defaultServerConfig,
initialCreditsExpirationWarningDays: warningThresholdDays,
},
});
});

const setupWorkspace = (
exhausted: boolean,
expired: boolean,
expiringSoon: boolean,
ownedByMe: boolean
) => {
// Set expiration date to be in the past if expired, in the future if not.
// If expiring soon, set it to be within the warning threshold, and just outside otherwise.
// Expired and expiringSoon are mutually exclusive.
const daysUntilExpiration = expired
? -1
: warningThresholdDays + (expiringSoon ? -1 : 1);
currentWorkspaceStore.next({
...workspaceDataStub,
initialCredits: {
exhausted,
expired,
expirationEpochMillis: plusDays(Date.now(), daysUntilExpiration),
},
creator: ownedByMe ? me : someOneElse,
});
};

/* All banners have "initial credits" in the text. Banner text can have one or more links in it.
* React Testing Library has a hard time finding text that is split across multiple elements
* (like text and links), so we can't use getByText to find the banner text. Instead, this
* function will return the textContent of the element that contains the banner text. This will
* include the plain text and the text found in the links.
*/

const getBannerText = () =>
screen.getAllByText(/initial credits/).pop().textContent;

const setProfileExtensionEligibility = (isEligible: boolean) => {
profileStore.set({
profile: {
...ProfileStubVariables.PROFILE_STUB,
eligibleForInitialCreditsExtension: isEligible,
},
load,
reload,
updateCache,
});
};

it('should show expiring soon banner to user who created the workspace', async () => {
const exhausted = false;
const expired = false;
const expiringSoon = true;
const ownedByMe = true;
setupWorkspace(exhausted, expired, expiringSoon, ownedByMe);
setProfileExtensionEligibility(true);

component();

await screen.findByText('Workspace credits are expiring soon');
expect(getBannerText()).toMatch(
'Your initial credits are expiring soon. You can request an extension here. For more ' +
'information, read the Using All of Us Initial Credits article on the User Support Hub.'
);
});

it('should show expiring soon banner to user who did not create the workspace', async () => {
const exhausted = false;
const expired = false;
const expiringSoon = true;
const ownedByMe = false;
setupWorkspace(exhausted, expired, expiringSoon, ownedByMe);
setProfileExtensionEligibility(true);

component();

await screen.findByText('Workspace credits are expiring soon');
expect(getBannerText()).toMatch(
'This workspace creator’s initial credits are expiring soon. This workspace was ' +
'created by someOneElse@fake-research-aou.org. You can request an extension here. For more information, ' +
'read the Using All of Us Initial Credits article on the User Support Hub.'
);
});

it('should show expired banner with option to extend to eligible user who created the workspace', async () => {
const exhausted = false;
const expired = true;
const expiringSoon = false;
const ownedByMe = true;
setupWorkspace(exhausted, expired, expiringSoon, ownedByMe);
setProfileExtensionEligibility(true);

component();

await screen.findByText('Workspace credits have expired');
expect(getBannerText()).toMatch(
'Your initial credits have expired. You can request an extension here. For more ' +
'information, read the Using All of Us Initial Credits article on the User Support Hub.'
);
});

it('should show expired banner to user who did not create the workspace and the owner is eligible for extension', async () => {
const exhausted = false;
const expired = true;
const expiringSoon = false;
const ownedByMe = false;
setupWorkspace(exhausted, expired, expiringSoon, ownedByMe);
setProfileExtensionEligibility(true);

component();

await screen.findByText('Workspace credits have expired');
expect(getBannerText()).toMatch(
'This workspace creator’s initial credits have expired. This workspace was created by ' +
'someOneElse@fake-research-aou.org. You can request an extension here. For more information, read the ' +
'Using All of Us Initial Credits article on the User Support Hub.'
);
});

it('should show expired banner with no option to extend to ineligible user who created the workspace', async () => {
const exhausted = false;
const expired = true;
const expiringSoon = false;
const ownedByMe = true;
setupWorkspace(exhausted, expired, expiringSoon, ownedByMe);
setProfileExtensionEligibility(false);

component();

await screen.findByText('This workspace is out of initial credits');
expect(getBannerText()).toMatch(
'Your initial credits have run out. To use the workspace, a valid billing account needs ' +
'to be provided. To learn more about establishing a billing account, read the Paying for Your ' +
'Research article on the User Support Hub.'
);
});

it('should show expired banner to user who did not create the workspace and the owner is not eligible for extension', async () => {
const exhausted = false;
const expired = true;
const expiringSoon = false;
const ownedByMe = false;
setupWorkspace(exhausted, expired, expiringSoon, ownedByMe);
setProfileExtensionEligibility(false);

component();

await screen.findByText('This workspace is out of initial credits');
expect(getBannerText()).toMatch(
'This workspace creator’s initial credits have run out. This workspace was created by ' +
'someOneElse@fake-research-aou.org. To use the workspace, a valid billing account needs to be provided. ' +
'To learn more about establishing a billing account, read the Paying for Your Research article ' +
'on the User Support Hub.'
);
});
});
Loading