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
133 changes: 133 additions & 0 deletions api/docs/space-api-docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2319,6 +2319,13 @@ paths:
type: string
description: Filter contracts by user email (case-insensitive regex match)
example: john@example.com
- name: groupId
in: query
required: false
schema:
type: string
description: Filter contracts by group ID
example: my-group
- name: page
in: query
required: false
Expand Down Expand Up @@ -2446,6 +2453,47 @@ paths:
'403':
description: Forbidden

put:
summary: Update contracts by group ID
description: |
Updates the details of all contracts in a group (contract novation - subscription composition change).

**Authentication**: User API Key (ADMIN) | Organization API Key

**User Permission**:
- ADMIN user: can update contracts in any organization

**Organization Permission**:
- ALL scope: can update contracts in the organization
- MANAGEMENT scope: can update contracts in the organization

tags:
- Contracts
security:
- ApiKeyAuth: []
parameters:
- name: userId
in: path
required: true
schema:
type: string
- name: groupId
in: query
required: true
schema:
type: string
requestBody:
$ref: '#/components/requestBodies/SubscriptionCompositionNovation'
responses:
'200':
description: Contracts updated
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Contract'

delete:
summary: Delete all contracts in organization
description: |
Expand Down Expand Up @@ -2637,6 +2685,13 @@ paths:
type: string
description: Filter contracts by user email (case-insensitive regex match)
example: john@example.com
- name: groupId
in: query
required: false
schema:
type: string
description: Filter contracts by group ID
example: my-group
- name: page
in: query
required: false
Expand Down Expand Up @@ -2774,6 +2829,47 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Contract'

put:
summary: Update contracts by group ID
description: |
Updates the details of all contracts in a group (contract novation - subscription composition change).

**Authentication**: User API Key (ADMIN) | Organization API Key

**User Permission**:
- ADMIN user: can update contracts in any organization

**Organization Permission**:
- ALL scope: can update contracts in the organization
- MANAGEMENT scope: can update contracts in the organization

tags:
- Contracts
security:
- ApiKeyAuth: []
parameters:
- name: userId
in: path
required: true
schema:
type: string
- name: groupId
in: query
required: true
schema:
type: string
requestBody:
$ref: '#/components/requestBodies/SubscriptionCompositionNovation'
responses:
'200':
description: Contracts updated
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Contract'

delete:
summary: Delete all contracts
Expand All @@ -2796,6 +2892,43 @@ paths:
'204':
description: All contracts deleted

/contracts/billingPeriod:
put:
summary: Bulk update billing period
description: |
Updates the billing period configuration for all contracts in a group.

**Authentication**: User API Key (ADMIN) | Organization API Key

**User Permission**:
- ADMIN user: can update billing period in any organization

**Organization Permission**:
- ALL scope: can update billing period in the organization
- MANAGEMENT scope: can update billing period in the organization

tags:
- Contracts
security:
- ApiKeyAuth: []
parameters:
- name: groupId
in: query
required: true
schema:
type: string
requestBody:
$ref: '#/components/requestBodies/SubscriptionBillingNovation'
responses:
'200':
description: Billing period updated for all contracts in the group
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Contract'

/contracts/{userId}:
get:
summary: Get contract
Expand Down
8 changes: 1 addition & 7 deletions api/src/main/config/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,13 +174,7 @@ export const ROUTE_PERMISSIONS: RoutePermission[] = [
// ============================================
{
path: '/contracts',
methods: ['GET'],
allowedUserRoles: ['ADMIN'],
allowedOrgRoles: ['ALL', 'MANAGEMENT'],
},
{
path: '/contracts',
methods: ['POST'],
methods: ['GET', 'POST', 'PUT'],
allowedUserRoles: ['ADMIN'],
allowedOrgRoles: ['ALL', 'MANAGEMENT'],
},
Expand Down
79 changes: 76 additions & 3 deletions api/src/main/controllers/ContractController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ class ContractController {
this.show = this.show.bind(this);
this.create = this.create.bind(this);
this.novate = this.novate.bind(this);
this.novateByGroupId = this.novateByGroupId.bind(this);
this.novateUserContact = this.novateUserContact.bind(this);
this.novateBillingPeriod = this.novateBillingPeriod.bind(this);
this.novateBillingPeriodByGroupId = this.novateBillingPeriodByGroupId.bind(this);
this.resetUsageLevels = this.resetUsageLevels.bind(this);
this.prune = this.prune.bind(this);
this.destroy = this.destroy.bind(this);
Expand Down Expand Up @@ -70,7 +72,23 @@ class ContractController {
const contractData: ContractToCreate = req.body;
const authOrganizationId = req.org?.id ?? req.params.organizationId;

contractData.organizationId = authOrganizationId;
if (
contractData.organizationId &&
authOrganizationId &&
contractData.organizationId !== authOrganizationId &&
!(req.user && req.user.role === 'ADMIN')
) {
throw new Error('PERMISSION ERROR: Organization ID mismatch');
}


if (!(req.user && req.user.role === 'ADMIN')){
contractData.organizationId = authOrganizationId;
}else{
if (!contractData.organizationId) {
throw new Error('INVALID DATA: Organization ID is required for contract creation. Since you are an ADMIN, you must specify the organizationId in the request body.');
}
}

const contract = await this.contractService.create(contractData);
res.status(201).json(contract);
Expand All @@ -82,7 +100,7 @@ class ContractController {
} else if (err.message.toLowerCase().includes('conflict:')) {
res.status(409).send({ error: err.message });
} else {
res.status(500).send({ error: err.message });
res.status(500).send({ error: err.message });
}
}
}
Expand All @@ -104,6 +122,31 @@ class ContractController {
}
}

async novateByGroupId(req: any, res: any) {
try {
const groupId = req.query.groupId;
const organizationId = req.org?.id ?? req.params.organizationId;
if (!groupId) {
res.status(400).send({ error: 'Missing groupId query parameter' });
return;
}
const newSubscription: Subscription = req.body;
const contracts = await this.contractService.novateByGroupId(groupId, organizationId, newSubscription);
res.status(200).json(contracts);
} catch (err: any) {
if (
err.message.toLowerCase().includes('not found') ||
err.message.toLowerCase().includes('no contracts found')
) {
res.status(404).send({ error: err.message });
} else if (err.message.toLowerCase().includes('invalid subscription:')) {
res.status(400).send({ error: err.message });
} else {
res.status(500).send({ error: err.message });
}
}
}

async novateUserContact(req: any, res: any) {
try {
const userId = req.params.userId;
Expand All @@ -123,11 +166,41 @@ class ContractController {
try {
const userId = req.params.userId;
const billingPeriod = req.body;

const contract = await this.contractService.novateBillingPeriod(userId, billingPeriod);
res.status(200).json(contract);
} catch (err: any) {
if (err.message.toLowerCase().includes('not found')) {
res.status(404).send({ error: err.message });
} else if (err.message.toLowerCase().includes('invalid data:')) {
res.status(400).send({ error: err.message });
} else {
res.status(500).send({ error: err.message });
}
}
}

async novateBillingPeriodByGroupId(req: any, res: any) {
try {
const groupId = req.query.groupId;
const organizationId = req.org?.id ?? req.params.organizationId;
const billingPeriod = req.body;

if (!groupId) {
res.status(400).send({ error: 'Missing groupId query parameter' });
return;
}

const contracts = await this.contractService.novateBillingPeriodByGroupId(groupId, organizationId, billingPeriod);
res.status(200).json(contracts);
} catch (err: any) {
if (
err.message.toLowerCase().includes('not found') ||
err.message.toLowerCase().includes('no contracts found')
) {
res.status(404).send({ error: err.message });
} else if (err.message.toLowerCase().includes('invalid data:')) {
res.status(400).send({ error: err.message });
} else {
res.status(500).send({ error: err.message });
}
Expand Down Expand Up @@ -158,7 +231,6 @@ class ContractController {

async prune(req: any, res: any) {
try {

const organizationId = req.org?.id ?? req.params.organizationId;

const result: number = await this.contractService.prune(organizationId, req.user);
Expand Down Expand Up @@ -196,6 +268,7 @@ class ContractController {
firstName: indexQueryParams.firstName as string,
lastName: indexQueryParams.lastName as string,
email: indexQueryParams.email as string,
groupId: indexQueryParams.groupId as string,
page: parseInt(indexQueryParams['page'] as string) || 1,
offset: parseInt(indexQueryParams['offset'] as string) || 0,
limit: parseInt(indexQueryParams['limit'] as string) || 20,
Expand Down
4 changes: 4 additions & 0 deletions api/src/main/repositories/mongoose/ContractRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const buildMatchPipeline = (queryFilters: any): ContractMatchPipeline => {
order = 'asc',
filters,
organizationId,
groupId,
} = queryFilters || {};

const matchConditions: any[] = [];
Expand All @@ -45,6 +46,9 @@ const buildMatchPipeline = (queryFilters: any): ContractMatchPipeline => {
if (organizationId) {
matchConditions.push({ organizationId: organizationId });
}
if (groupId) {
matchConditions.push({ groupId: groupId });
}

const pipeline: any[] = [
{ $addFields: { contractedServicesArray: { $objectToArray: '$contractedServices' } } },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const contractSchema = new Schema(
},
usageLevels: {type: Map, of: {type: Map, of: usageLevelSchema}},
organizationId: { type: String, ref: "Organization", required: true },
groupId: { type: String },
contractedServices: {type: Map, of: String},
subscriptionPlans: { type: Map, of: String },
subscriptionAddOns: { type: Map, of: {type: Map, of: Number} },
Expand Down
Loading
Loading