Skip to content

Commit 5d5e0b3

Browse files
authored
feat: recruiter organization seats (#3352)
1 parent b2ed124 commit 5d5e0b3

12 files changed

Lines changed: 546 additions & 76 deletions

File tree

__tests__/schema/opportunity.ts

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ import type { ServiceClient } from '../../src/types';
8181
import { OpportunityJob } from '../../src/entity/opportunities/OpportunityJob';
8282
import * as brokkrCommon from '../../src/common/brokkr';
8383
import { randomUUID } from 'node:crypto';
84+
import { updateRecruiterSubscriptionFlags } from '../../src/common';
85+
import { SubscriptionStatus } from '../../src/common/plus';
8486

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

5060+
await con.getRepository(Organization).update(
5061+
{
5062+
id: organizationsFixture[0].id,
5063+
},
5064+
{
5065+
recruiterSubscriptionFlags:
5066+
updateRecruiterSubscriptionFlags<Organization>({
5067+
subscriptionId: 'sub_test',
5068+
status: SubscriptionStatus.Active,
5069+
items: [
5070+
{
5071+
priceId: 'test',
5072+
quantity: 1,
5073+
},
5074+
],
5075+
}),
5076+
},
5077+
);
5078+
50585079
await testMutationErrorCode(
50595080
client,
50605081
{
@@ -5094,6 +5115,25 @@ describe('mutation updateOpportunityState', () => {
50945115

50955116
const opportunityId = opportunitiesFixture[3].id;
50965117

5118+
await con.getRepository(Organization).update(
5119+
{
5120+
id: opportunitiesFixture[3].organizationId!,
5121+
},
5122+
{
5123+
recruiterSubscriptionFlags:
5124+
updateRecruiterSubscriptionFlags<Organization>({
5125+
subscriptionId: 'sub_test',
5126+
status: SubscriptionStatus.Active,
5127+
items: [
5128+
{
5129+
priceId: 'test',
5130+
quantity: 1,
5131+
},
5132+
],
5133+
}),
5134+
},
5135+
);
5136+
50975137
await con.getRepository(OpportunityUser).save({
50985138
opportunityId,
50995139
userId: '1',
@@ -5191,6 +5231,168 @@ describe('mutation updateOpportunityState', () => {
51915231
'Opportunity must have an organization assigned',
51925232
);
51935233
});
5234+
5235+
it('should update state to CLOSED state', async () => {
5236+
loggedUser = '1';
5237+
5238+
const opportunity = await con.getRepository(OpportunityJob).save({
5239+
title: 'Test',
5240+
tldr: 'Test',
5241+
state: OpportunityState.LIVE,
5242+
organizationId: organizationsFixture[0].id,
5243+
});
5244+
5245+
await con.getRepository(OpportunityUser).save({
5246+
opportunityId: opportunity.id,
5247+
userId: '1',
5248+
type: OpportunityUserType.Recruiter,
5249+
});
5250+
5251+
await con.getRepository(Organization).update(
5252+
{
5253+
id: organizationsFixture[0].id,
5254+
},
5255+
{
5256+
recruiterSubscriptionFlags:
5257+
updateRecruiterSubscriptionFlags<Organization>({
5258+
subscriptionId: 'sub_test',
5259+
status: SubscriptionStatus.Active,
5260+
items: [
5261+
{
5262+
priceId: 'test',
5263+
quantity: 1,
5264+
},
5265+
],
5266+
}),
5267+
},
5268+
);
5269+
5270+
const res = await client.mutate(MUTATION, {
5271+
variables: { id: opportunity.id, state: OpportunityState.CLOSED },
5272+
});
5273+
5274+
expect(res.errors).toBeFalsy();
5275+
5276+
const after = await con
5277+
.getRepository(Opportunity)
5278+
.findOneByOrFail({ id: opportunity.id });
5279+
expect(after.state).toBe(OpportunityState.CLOSED);
5280+
});
5281+
5282+
it('should throw conflict on CLOSED transition when subscription is missing', async () => {
5283+
loggedUser = '1';
5284+
5285+
const opportunity = await con.getRepository(OpportunityJob).save({
5286+
title: 'Test',
5287+
tldr: 'Test',
5288+
state: OpportunityState.LIVE,
5289+
organizationId: organizationsFixture[0].id,
5290+
});
5291+
5292+
await con.getRepository(OpportunityUser).save({
5293+
opportunityId: opportunity.id,
5294+
userId: '1',
5295+
type: OpportunityUserType.Recruiter,
5296+
});
5297+
5298+
await testMutationErrorCode(
5299+
client,
5300+
{
5301+
mutation: MUTATION,
5302+
variables: { id: opportunity.id, state: OpportunityState.CLOSED },
5303+
},
5304+
'CONFLICT',
5305+
'Opportunity subscription not found',
5306+
);
5307+
});
5308+
5309+
it('should throw conflict on LIVE transition when subscription is not active yet', async () => {
5310+
loggedUser = '1';
5311+
5312+
const opportunity = await con.getRepository(OpportunityJob).save({
5313+
title: 'Test',
5314+
tldr: 'Test',
5315+
state: OpportunityState.DRAFT,
5316+
organizationId: organizationsFixture[0].id,
5317+
});
5318+
5319+
await con.getRepository(OpportunityUser).save({
5320+
opportunityId: opportunity.id,
5321+
userId: '1',
5322+
type: OpportunityUserType.Recruiter,
5323+
});
5324+
5325+
await testMutationErrorCode(
5326+
client,
5327+
{
5328+
mutation: MUTATION,
5329+
variables: { id: opportunity.id, state: OpportunityState.LIVE },
5330+
},
5331+
'CONFLICT',
5332+
'Opportunity subscription is not active yet, make sure your payment was processed in full. Contact support if the issue persists.',
5333+
);
5334+
});
5335+
5336+
it('should throw payment required on LIVE transition when no more allowed seats', async () => {
5337+
loggedUser = '1';
5338+
5339+
const opportunityId = opportunitiesFixture[3].id;
5340+
5341+
await con.getRepository(Organization).update(
5342+
{
5343+
id: opportunitiesFixture[3].organizationId!,
5344+
},
5345+
{
5346+
recruiterSubscriptionFlags:
5347+
updateRecruiterSubscriptionFlags<Organization>({
5348+
subscriptionId: 'sub_test',
5349+
status: SubscriptionStatus.Active,
5350+
items: [],
5351+
}),
5352+
},
5353+
);
5354+
5355+
await con.getRepository(OpportunityUser).save({
5356+
opportunityId,
5357+
userId: '1',
5358+
type: OpportunityUserType.Recruiter,
5359+
});
5360+
5361+
await con.getRepository(OpportunityKeyword).save({
5362+
opportunityId,
5363+
keyword: 'typescript',
5364+
});
5365+
await con.getRepository(QuestionScreening).save({
5366+
opportunityId,
5367+
title: 'Tell us about a recent project',
5368+
questionOrder: 0,
5369+
});
5370+
await con.getRepository(Opportunity).update(
5371+
{ id: opportunityId },
5372+
{
5373+
content: {
5374+
overview: { content: 'Overview content', html: '' },
5375+
responsibilities: { content: 'Responsibilities content', html: '' },
5376+
requirements: { content: 'Requirements content', html: '' },
5377+
},
5378+
},
5379+
);
5380+
5381+
const before = await con
5382+
.getRepository(Opportunity)
5383+
.findOneByOrFail({ id: opportunityId });
5384+
expect(before.state).toBe(OpportunityState.DRAFT);
5385+
5386+
await testMutationErrorCode(
5387+
client,
5388+
{
5389+
mutation: MUTATION,
5390+
variables: { id: opportunityId, state: OpportunityState.LIVE },
5391+
},
5392+
'PAYMENT_REQUIRED',
5393+
'Your subscription allows for 0 live opportunities. Please upgrade your subscription to add more or pause other live opportunities.',
5394+
);
5395+
});
51945396
});
51955397

51965398
describe('mutation parseOpportunity', () => {

seeds/ExperimentVariant.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"feature": "recruiter_pricing_ids",
2525
"variant": "recruiter_default",
2626
"createdAt": "2025-12-10T13:45:08.395Z",
27-
"value": "[{\"title\":\"Job Slot (Starter Tier)\",\"appsId\":\"\",\"idMap\":{\"paddle\":\"pri_01kbq0p5g7qw8zb1e8esf7qjw2\",\"ios\":\"\"}},{\"title\":\"Job Slot (Boost Tier)\",\"appsId\":\"\",\"idMap\":{\"paddle\":\"pri_01kbq0pv4mxbar7qt1nzs694r1\",\"ios\":\"\"}}]",
27+
"value": "[{\"title\":\"Starter\",\"appsId\":\"\",\"idMap\":{\"paddle\":\"pri_01kbq0p5g7qw8zb1e8esf7qjw2\",\"ios\":\"\"}},{\"title\":\"Boost\",\"appsId\":\"\",\"idMap\":{\"paddle\":\"pri_01kbq0pv4mxbar7qt1nzs694r1\",\"ios\":\"\"}}]",
2828
"type": "productPricing"
2929
}
3030
]

0 commit comments

Comments
 (0)