Skip to content

Commit 8df734d

Browse files
authored
Merge pull request #79 from StreetSupport/feature/client-groups-endpoint
Add client groups endpoint and service denormalisation
2 parents 8de680d + 4d72feb commit 8df734d

File tree

10 files changed

+135
-8
lines changed

10 files changed

+135
-8
lines changed

src/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import bannerRoutes from './routes/bannerRoutes.js';
1010
import swepBannerRoutes from './routes/swepBannerRoutes.js';
1111
import resourceRoutes from './routes/resourceRoutes.js';
1212
import locationLogoRoutes from './routes/locationLogoRoutes.js';
13+
import clientGroupRoutes from './routes/clientGroupRoutes.js';
1314
import { errorHandler, notFound } from './middleware/errorMiddleware.js';
1415
import checkJwt from './middleware/checkJwt.js';
1516
import './instrument.js';
@@ -34,6 +35,7 @@ app.use('/api/banners', bannerRoutes);
3435
app.use('/api/swep-banners', swepBannerRoutes);
3536
app.use('/api/resources', resourceRoutes);
3637
app.use('/api/location-logos', locationLogoRoutes);
38+
app.use('/api/client-groups', clientGroupRoutes);
3739

3840
// The error handler must be registered before any other error middleware and after all controllers
3941
Sentry.setupExpressErrorHandler(app);
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Request, Response } from 'express';
2+
import { asyncHandler } from '../utils/asyncHandler.js';
3+
import { sendSuccess, sendNotFound, sendInternalError } from '../utils/apiResponses.js';
4+
import ClientGroup from '../models/clientGroupModel.js';
5+
6+
/**
7+
* @desc Get all client groups
8+
* @route GET /api/client-groups
9+
* @access Private
10+
*/
11+
export const getClientGroups = asyncHandler(async (req: Request, res: Response) => {
12+
try {
13+
const clientGroups = await ClientGroup.find({})
14+
.sort({ SortPosition: -1 })
15+
.lean();
16+
17+
if (!clientGroups || clientGroups.length === 0) {
18+
return sendNotFound(res, 'No client groups found');
19+
}
20+
21+
return sendSuccess(res, clientGroups);
22+
} catch (error) {
23+
console.error('Error fetching client groups:', error);
24+
return sendInternalError(res, 'Failed to fetch client groups');
25+
}
26+
});

src/controllers/serviceController.ts

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,30 @@ import { asyncHandler } from '../utils/asyncHandler.js';
44
import { sendSuccess, sendCreated, sendNotFound, sendBadRequest } from '../utils/apiResponses.js';
55
import GroupedService from '../models/groupedServiceModel.js';
66
import Service from '../models/serviceModel.js';
7+
import ClientGroup from '../models/clientGroupModel.js';
78
import { processAddressesWithCoordinates, updateLocationIfPostcodeChanged } from '../utils/postcodeValidation.js';
89
import { validateGroupedService } from '../schemas/groupedServiceSchema.js';
9-
import { IGroupedService } from '../types/index.js';
10+
import { IGroupedService, IClientGroupRef } from '../types/index.js';
11+
12+
/**
13+
* Helper function to denormalise client groups from keys
14+
* Looks up client group documents and returns denormalised data
15+
*/
16+
async function denormaliseClientGroups(clientGroupKeys?: string[]): Promise<IClientGroupRef[]> {
17+
if (!clientGroupKeys || clientGroupKeys.length === 0) {
18+
return [];
19+
}
20+
21+
const clientGroups = await ClientGroup.find({
22+
Key: { $in: clientGroupKeys }
23+
}).lean();
24+
25+
return clientGroups.map(cg => ({
26+
Key: cg.Key,
27+
Name: cg.Name,
28+
SortPosition: cg.SortPosition
29+
}));
30+
}
1031

1132
// @desc Get all services
1233
// @route GET /api/services
@@ -152,8 +173,16 @@ export const createService = asyncHandler(async (req: Request, res: Response) =>
152173
await processAddressesWithCoordinates([serviceData.Location]);
153174
}
154175

176+
// Denormalise client groups if keys provided
177+
const clientGroups = serviceData.ClientGroupKeys && serviceData.ClientGroupKeys.length > 0
178+
? await denormaliseClientGroups(serviceData.ClientGroupKeys)
179+
: [];
180+
155181
// Create the grouped service
156-
const groupedService = await GroupedService.create([serviceData], { session });
182+
const groupedService = await GroupedService.create([{
183+
...serviceData,
184+
ClientGroups: clientGroups
185+
}], { session });
157186

158187
// Create individual ProvidedServices for each subcategory
159188
await createIndividualServices(groupedService[0], session);
@@ -207,12 +236,12 @@ export const updateService = asyncHandler(async (req: Request, res: Response) =>
207236
// Check if location postcode has changed and update coordinates accordingly
208237
if (updateData.Location && updateData.Location.Postcode) {
209238
const oldLocation = existingService.Location;
210-
239+
211240
if (oldLocation && oldLocation.Postcode) {
212241
// Update location if postcode changed
213242
await updateLocationIfPostcodeChanged(
214-
oldLocation.Postcode,
215-
updateData.Location.Postcode,
243+
oldLocation.Postcode,
244+
updateData.Location.Postcode,
216245
updateData.Location
217246
);
218247
} else if (!updateData.Location.Location) {
@@ -221,10 +250,18 @@ export const updateService = asyncHandler(async (req: Request, res: Response) =>
221250
}
222251
}
223252

253+
// Denormalise client groups if keys provided
254+
const clientGroups = updateData.ClientGroupKeys
255+
? await denormaliseClientGroups(updateData.ClientGroupKeys)
256+
: [];
257+
224258
// Update the grouped service
225259
const updatedService = await GroupedService.findByIdAndUpdate(
226-
req.params.id,
227-
updateData,
260+
req.params.id,
261+
{
262+
...updateData,
263+
ClientGroups: clientGroups
264+
},
228265
{ new: true, session }
229266
);
230267

src/models/clientGroupModel.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import mongoose from "mongoose";
2+
import { IClientGroup } from "../types/IClientGroup.js";
3+
4+
const clientGroupSchema = new mongoose.Schema({
5+
Key: { type: String, required: true },
6+
Name: { type: String, required: true },
7+
SortPosition: { type: Number, required: true },
8+
DocumentCreationDate: { type: Date, required: false },
9+
DocumentModifiedDate: { type: Date, required: false },
10+
CreatedBy: { type: String, required: false }
11+
}, { collection: 'ClientGroups', versionKey: false });
12+
13+
const ClientGroup = mongoose.model<IClientGroup>("ClientGroups", clientGroupSchema);
14+
15+
export default ClientGroup;

src/models/groupedServiceModel.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,18 @@ const groupedServiceSchema = new Schema<IGroupedService>({
8585
Telephone: {
8686
type: String,
8787
required: false,
88+
},
89+
ClientGroupKeys: {
90+
type: [String],
91+
required: false,
92+
},
93+
ClientGroups: {
94+
type: [{
95+
Key: { type: String, required: true },
96+
Name: { type: String, required: true },
97+
SortPosition: { type: Number, required: true }
98+
}],
99+
required: false,
88100
}
89101
}, { collection: 'GroupedProvidedServices', versionKey: false });
90102

src/routes/clientGroupRoutes.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import express from 'express';
2+
import { getClientGroups } from '../controllers/clientGroupController.js';
3+
import { authenticate } from '../middleware/authMiddleware.js';
4+
5+
const router = express.Router();
6+
7+
/**
8+
* @route GET /api/client-groups
9+
* @desc Get all client groups
10+
* @access Private
11+
*/
12+
router.get('/', authenticate, getClientGroups);
13+
14+
export default router;

src/schemas/groupedServiceSchema.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ export const GroupedServiceSchema = z.object({
9292
SubCategories: z.array(ServiceSubCategorySchema).min(1, 'At least one subcategory is required'),
9393
IsTelephoneService: z.boolean().optional().default(false),
9494
IsAppointmentOnly: z.boolean().optional().default(false),
95-
Telephone: z.preprocess(preprocessNullableString, z.string().optional())
95+
Telephone: z.preprocess(preprocessNullableString, z.string().optional()),
96+
ClientGroupKeys: z.array(z.string()).optional()
9697
}).refine((data) => {
9798
// If not open 24/7, not appointment only, and not outreach location, require opening times
9899
if (!data.IsOpen247 && !data.IsAppointmentOnly && !data.Location.IsOutreachLocation) {

src/types/IClientGroup.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Document } from "mongoose";
2+
3+
export interface IClientGroup extends Document {
4+
_id: string;
5+
Key: string;
6+
Name: string;
7+
SortPosition: number;
8+
DocumentCreationDate?: Date;
9+
DocumentModifiedDate?: Date;
10+
CreatedBy?: string;
11+
}

src/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export * from './ICity.js';
33
export * from './IFaq.js';
44
export * from './IUser.js';
55
export * from './ILocationLogo.js';
6+
export * from './IClientGroup.js';
67

78

89
// Service provider related types

src/types/organisations/IGroupedService.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ import { IOpeningTime } from "./IOpeningTime.js";
33
import { ILocation } from "./ILocation.js";
44
import { IServiceSubCategory } from "./IServiceSubCategory.js";
55

6+
export interface IClientGroupRef {
7+
Key: string;
8+
Name: string;
9+
SortPosition: number;
10+
}
11+
612
export interface IGroupedService extends Document {
713
_id: Types.ObjectId;
814
DocumentCreationDate: Date;
@@ -24,4 +30,6 @@ export interface IGroupedService extends Document {
2430
IsTelephoneService?: boolean;
2531
IsAppointmentOnly?: boolean;
2632
Telephone?: string;
33+
ClientGroupKeys?: string[];
34+
ClientGroups?: IClientGroupRef[];
2735
}

0 commit comments

Comments
 (0)