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
202 changes: 202 additions & 0 deletions __tests__/schema/opportunity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ import type { ServiceClient } from '../../src/types';
import { OpportunityJob } from '../../src/entity/opportunities/OpportunityJob';
import * as brokkrCommon from '../../src/common/brokkr';
import { randomUUID } from 'node:crypto';
import { updateRecruiterSubscriptionFlags } from '../../src/common';
import { SubscriptionStatus } from '../../src/common/plus';

// Mock Slack WebClient
const mockConversationsCreate = jest.fn();
Expand Down Expand Up @@ -5055,6 +5057,25 @@ describe('mutation updateOpportunityState', () => {
type: OpportunityUserType.Recruiter,
});

await con.getRepository(Organization).update(
{
id: organizationsFixture[0].id,
},
{
recruiterSubscriptionFlags:
updateRecruiterSubscriptionFlags<Organization>({
subscriptionId: 'sub_test',
status: SubscriptionStatus.Active,
items: [
{
priceId: 'test',
quantity: 1,
},
],
}),
},
);

await testMutationErrorCode(
client,
{
Expand Down Expand Up @@ -5094,6 +5115,25 @@ describe('mutation updateOpportunityState', () => {

const opportunityId = opportunitiesFixture[3].id;

await con.getRepository(Organization).update(
{
id: opportunitiesFixture[3].organizationId!,
},
{
recruiterSubscriptionFlags:
updateRecruiterSubscriptionFlags<Organization>({
subscriptionId: 'sub_test',
status: SubscriptionStatus.Active,
items: [
{
priceId: 'test',
quantity: 1,
},
],
}),
},
);

await con.getRepository(OpportunityUser).save({
opportunityId,
userId: '1',
Expand Down Expand Up @@ -5191,6 +5231,168 @@ describe('mutation updateOpportunityState', () => {
'Opportunity must have an organization assigned',
);
});

it('should update state to CLOSED state', async () => {
loggedUser = '1';

const opportunity = await con.getRepository(OpportunityJob).save({
title: 'Test',
tldr: 'Test',
state: OpportunityState.LIVE,
organizationId: organizationsFixture[0].id,
});

await con.getRepository(OpportunityUser).save({
opportunityId: opportunity.id,
userId: '1',
type: OpportunityUserType.Recruiter,
});

await con.getRepository(Organization).update(
{
id: organizationsFixture[0].id,
},
{
recruiterSubscriptionFlags:
updateRecruiterSubscriptionFlags<Organization>({
subscriptionId: 'sub_test',
status: SubscriptionStatus.Active,
items: [
{
priceId: 'test',
quantity: 1,
},
],
}),
},
);

const res = await client.mutate(MUTATION, {
variables: { id: opportunity.id, state: OpportunityState.CLOSED },
});

expect(res.errors).toBeFalsy();

const after = await con
.getRepository(Opportunity)
.findOneByOrFail({ id: opportunity.id });
expect(after.state).toBe(OpportunityState.CLOSED);
});

it('should throw conflict on CLOSED transition when subscription is missing', async () => {
loggedUser = '1';

const opportunity = await con.getRepository(OpportunityJob).save({
title: 'Test',
tldr: 'Test',
state: OpportunityState.LIVE,
organizationId: organizationsFixture[0].id,
});

await con.getRepository(OpportunityUser).save({
opportunityId: opportunity.id,
userId: '1',
type: OpportunityUserType.Recruiter,
});

await testMutationErrorCode(
client,
{
mutation: MUTATION,
variables: { id: opportunity.id, state: OpportunityState.CLOSED },
},
'CONFLICT',
'Opportunity subscription not found',
);
});

it('should throw conflict on LIVE transition when subscription is not active yet', async () => {
loggedUser = '1';

const opportunity = await con.getRepository(OpportunityJob).save({
title: 'Test',
tldr: 'Test',
state: OpportunityState.DRAFT,
organizationId: organizationsFixture[0].id,
});

await con.getRepository(OpportunityUser).save({
opportunityId: opportunity.id,
userId: '1',
type: OpportunityUserType.Recruiter,
});

await testMutationErrorCode(
client,
{
mutation: MUTATION,
variables: { id: opportunity.id, state: OpportunityState.LIVE },
},
'CONFLICT',
'Opportunity subscription is not active yet, make sure your payment was processed in full. Contact support if the issue persists.',
);
});

it('should throw payment required on LIVE transition when no more allowed seats', async () => {
loggedUser = '1';

const opportunityId = opportunitiesFixture[3].id;

await con.getRepository(Organization).update(
{
id: opportunitiesFixture[3].organizationId!,
},
{
recruiterSubscriptionFlags:
updateRecruiterSubscriptionFlags<Organization>({
subscriptionId: 'sub_test',
status: SubscriptionStatus.Active,
items: [],
}),
},
);

await con.getRepository(OpportunityUser).save({
opportunityId,
userId: '1',
type: OpportunityUserType.Recruiter,
});

await con.getRepository(OpportunityKeyword).save({
opportunityId,
keyword: 'typescript',
});
await con.getRepository(QuestionScreening).save({
opportunityId,
title: 'Tell us about a recent project',
questionOrder: 0,
});
await con.getRepository(Opportunity).update(
{ id: opportunityId },
{
content: {
overview: { content: 'Overview content', html: '' },
responsibilities: { content: 'Responsibilities content', html: '' },
requirements: { content: 'Requirements content', html: '' },
},
},
);

const before = await con
.getRepository(Opportunity)
.findOneByOrFail({ id: opportunityId });
expect(before.state).toBe(OpportunityState.DRAFT);

await testMutationErrorCode(
client,
{
mutation: MUTATION,
variables: { id: opportunityId, state: OpportunityState.LIVE },
},
'PAYMENT_REQUIRED',
'Your subscription allows for 0 live opportunities. Please upgrade your subscription to add more or pause other live opportunities.',
);
});
});

describe('mutation parseOpportunity', () => {
Expand Down
2 changes: 1 addition & 1 deletion seeds/ExperimentVariant.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"feature": "recruiter_pricing_ids",
"variant": "recruiter_default",
"createdAt": "2025-12-10T13:45:08.395Z",
"value": "[{\"title\":\"Job Slot (Starter Tier)\",\"appsId\":\"\",\"idMap\":{\"paddle\":\"pri_01kbq0p5g7qw8zb1e8esf7qjw2\",\"ios\":\"\"}},{\"title\":\"Job Slot (Boost Tier)\",\"appsId\":\"\",\"idMap\":{\"paddle\":\"pri_01kbq0pv4mxbar7qt1nzs694r1\",\"ios\":\"\"}}]",
"value": "[{\"title\":\"Starter\",\"appsId\":\"\",\"idMap\":{\"paddle\":\"pri_01kbq0p5g7qw8zb1e8esf7qjw2\",\"ios\":\"\"}},{\"title\":\"Boost\",\"appsId\":\"\",\"idMap\":{\"paddle\":\"pri_01kbq0pv4mxbar7qt1nzs694r1\",\"ios\":\"\"}}]",
"type": "productPricing"
}
]
Loading
Loading