@planning-inspectorate/data-model 2.40.0 → 2.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.example ADDED
@@ -0,0 +1,6 @@
1
+ LOAD_TEST_SIMULATE_SENDING=true
2
+ LOAD_TEST_SERVICE_BUS_NAMESPACE=###.servicebus.windows.net
3
+ APPEALS_LOAD_TEST_CASE_TYPE=S78
4
+ APPEALS_LOAD_TEST_CASE_COUNT=1
5
+ APPEALS_LOAD_TEST_DOCUMENTS_PER_CASE=2
6
+ APPEALS_LOAD_TEST_BATCH_SIZE=50
@@ -7,16 +7,16 @@
7
7
  | Property | Type | Required | Nullable | Defined by |
8
8
  | :------------------------------------------------ | :-------- | :------- | :------------- | :------------------------------------------------------------------------------------------------------------------------------ |
9
9
  | [caseId](#caseid) | `integer` | Required | cannot be null | [NSIP Invoice](nsip-invoice-properties-caseid.md "nsip-invoice.schema.json#/properties/caseId") |
10
- | [caseReference](#casereference) | `string` | Required | can be null | [NSIP Invoice](nsip-invoice-properties-casereference.md "nsip-invoice.schema.json#/properties/caseReference") |
11
- | [invoiceStage](#invoicestage) | `string` | Required | can be null | [NSIP Invoice](nsip-invoice-properties-invoicestage.md "nsip-invoice.schema.json#/properties/invoiceStage") |
10
+ | [caseReference](#casereference) | `string` | Optional | can be null | [NSIP Invoice](nsip-invoice-properties-casereference.md "nsip-invoice.schema.json#/properties/caseReference") |
11
+ | [invoiceStage](#invoicestage) | `string` | Required | cannot be null | [NSIP Invoice](nsip-invoice-properties-invoicestage.md "nsip-invoice.schema.json#/properties/invoiceStage") |
12
12
  | [invoiceNumber](#invoicenumber) | `string` | Required | cannot be null | [NSIP Invoice](nsip-invoice-properties-invoicenumber.md "nsip-invoice.schema.json#/properties/invoiceNumber") |
13
- | [amountDue](#amountdue) | `number` | Required | cannot be null | [NSIP Invoice](nsip-invoice-properties-amountdue.md "nsip-invoice.schema.json#/properties/amountDue") |
14
- | [paymentDueDate](#paymentduedate) | `string` | Required | can be null | [NSIP Invoice](nsip-invoice-properties-paymentduedate.md "nsip-invoice.schema.json#/properties/paymentDueDate") |
15
- | [invoicedDate](#invoiceddate) | `string` | Required | can be null | [NSIP Invoice](nsip-invoice-properties-invoiceddate.md "nsip-invoice.schema.json#/properties/invoicedDate") |
16
- | [paymentDate](#paymentdate) | `string` | Required | can be null | [NSIP Invoice](nsip-invoice-properties-paymentdate.md "nsip-invoice.schema.json#/properties/paymentDate") |
17
- | [refundCreditNoteNumber](#refundcreditnotenumber) | `string` | Required | can be null | [NSIP Invoice](nsip-invoice-properties-refundcreditnotenumber.md "nsip-invoice.schema.json#/properties/refundCreditNoteNumber") |
18
- | [refundAmount](#refundamount) | `number` | Required | can be null | [NSIP Invoice](nsip-invoice-properties-refundamount.md "nsip-invoice.schema.json#/properties/refundAmount") |
19
- | [refundIssueDate](#refundissuedate) | `string` | Required | can be null | [NSIP Invoice](nsip-invoice-properties-refundissuedate.md "nsip-invoice.schema.json#/properties/refundIssueDate") |
13
+ | [amountDue](#amountdue) | `number` | Optional | can be null | [NSIP Invoice](nsip-invoice-properties-amountdue.md "nsip-invoice.schema.json#/properties/amountDue") |
14
+ | [paymentDueDate](#paymentduedate) | `string` | Optional | can be null | [NSIP Invoice](nsip-invoice-properties-paymentduedate.md "nsip-invoice.schema.json#/properties/paymentDueDate") |
15
+ | [invoicedDate](#invoiceddate) | `string` | Optional | can be null | [NSIP Invoice](nsip-invoice-properties-invoiceddate.md "nsip-invoice.schema.json#/properties/invoicedDate") |
16
+ | [paymentDate](#paymentdate) | `string` | Optional | can be null | [NSIP Invoice](nsip-invoice-properties-paymentdate.md "nsip-invoice.schema.json#/properties/paymentDate") |
17
+ | [refundCreditNoteNumber](#refundcreditnotenumber) | `string` | Optional | can be null | [NSIP Invoice](nsip-invoice-properties-refundcreditnotenumber.md "nsip-invoice.schema.json#/properties/refundCreditNoteNumber") |
18
+ | [refundAmount](#refundamount) | `number` | Optional | can be null | [NSIP Invoice](nsip-invoice-properties-refundamount.md "nsip-invoice.schema.json#/properties/refundAmount") |
19
+ | [refundIssueDate](#refundissuedate) | `string` | Optional | can be null | [NSIP Invoice](nsip-invoice-properties-refundissuedate.md "nsip-invoice.schema.json#/properties/refundIssueDate") |
20
20
  | Additional Properties | Any | Optional | can be null | |
21
21
 
22
22
  ## caseId
@@ -49,7 +49,7 @@ The unique identifier within the Back Office. This is not the same as the case r
49
49
 
50
50
  `caseReference`
51
51
 
52
- * is required
52
+ * is optional
53
53
 
54
54
  * Type: `string`
55
55
 
@@ -77,7 +77,7 @@ Invoice stage
77
77
 
78
78
  * Type: `string`
79
79
 
80
- * can be null
80
+ * cannot be null
81
81
 
82
82
  * defined in: [NSIP Invoice](nsip-invoice-properties-invoicestage.md "nsip-invoice.schema.json#/properties/invoiceStage")
83
83
 
@@ -96,7 +96,6 @@ Invoice stage
96
96
  | `"pre_examination"` | |
97
97
  | `"initial_examination"` | |
98
98
  | `"final_examination"` | |
99
- | `null` | |
100
99
 
101
100
  ### invoiceStage Examples
102
101
 
@@ -134,11 +133,11 @@ Amount due
134
133
 
135
134
  `amountDue`
136
135
 
137
- * is required
136
+ * is optional
138
137
 
139
138
  * Type: `number`
140
139
 
141
- * cannot be null
140
+ * can be null
142
141
 
143
142
  * defined in: [NSIP Invoice](nsip-invoice-properties-amountdue.md "nsip-invoice.schema.json#/properties/amountDue")
144
143
 
@@ -158,7 +157,7 @@ Payment due date
158
157
 
159
158
  `paymentDueDate`
160
159
 
161
- * is required
160
+ * is optional
162
161
 
163
162
  * Type: `string`
164
163
 
@@ -186,7 +185,7 @@ Invoiced date
186
185
 
187
186
  `invoicedDate`
188
187
 
189
- * is required
188
+ * is optional
190
189
 
191
190
  * Type: `string`
192
191
 
@@ -214,7 +213,7 @@ Date of payment
214
213
 
215
214
  `paymentDate`
216
215
 
217
- * is required
216
+ * is optional
218
217
 
219
218
  * Type: `string`
220
219
 
@@ -242,7 +241,7 @@ Refund credit note number
242
241
 
243
242
  `refundCreditNoteNumber`
244
243
 
245
- * is required
244
+ * is optional
246
245
 
247
246
  * Type: `string`
248
247
 
@@ -266,7 +265,7 @@ Amount refunded
266
265
 
267
266
  `refundAmount`
268
267
 
269
- * is required
268
+ * is optional
270
269
 
271
270
  * Type: `number`
272
271
 
@@ -290,7 +289,7 @@ Refund issue date
290
289
 
291
290
  `refundIssueDate`
292
291
 
293
- * is required
292
+ * is optional
294
293
 
295
294
  * Type: `string`
296
295
 
@@ -0,0 +1,35 @@
1
+ # Appeals Load Test Script
2
+
3
+ This script is designed to allow load testing of service bus message consumers without the capacity of the sender being a limiting factor. The service bus client/strategy is hopefully resuable for other services in future
4
+
5
+ `load-test-appeals` sends a specified quantity of appeal case messages with service users and documents to service bus
6
+
7
+ This was written as a quick solution to provide ODW with the ability to run a load test without having to generate actual appeals within the Appeals Service, as generating them Appeals side proved too slow
8
+
9
+ ## Requirements
10
+
11
+ - node
12
+ - Azure VPN
13
+ - Azure CLI interface
14
+ - Your user has the ability to send messages manually on the given service bus topic
15
+ - Unsure if this can be run in WSL currently as az login seems to fail
16
+
17
+ ## Setup
18
+
19
+ `npm ci`
20
+
21
+ Create an .env file in the route of the project based on the example
22
+
23
+ - LOAD_TEST_SIMULATE_SENDING: (true|false) logs to console if true rather than sending, usful for checking data before sending
24
+ - LOAD_TEST_SERVICE_BUS_NAMESPACE: the fully qualified namespace of the service bus instance
25
+ - APPEALS_LOAD_TEST_CASE_TYPE: (HAS|S78) the case type topic to send on
26
+ - APPEALS_LOAD_TEST_CASE_COUNT: how many appeals to generate
27
+ - APPEALS_LOAD_TEST_DOCUMENTS_PER_CASE: how many documents to generate for each case
28
+ - APPEALS_LOAD_TEST_BATCH_SIZE: how many appeals are sent to service bus in one go, used to improve the speed in which messages can be sent to service bus
29
+
30
+ ## Run
31
+
32
+ - set the env vars as needed, retrieve the correct Service Bus namespace from Azure
33
+ - connect to Azure VPN
34
+ - `az login` - login as yourself and connect to test env subscription
35
+ - `npm run load-test-appeals`
@@ -0,0 +1,456 @@
1
+ import crypto from 'node:crypto';
2
+ import Ajv from 'ajv';
3
+ import addFormats from 'ajv-formats';
4
+ import { loadAllSchemasSync } from '../../src/index.js';
5
+
6
+ const HAS_SCHEMA = 'appeal-has.schema.json';
7
+ const S78_SCHEMA = 'appeal-s78.schema.json';
8
+ const DOCUMENT_SCHEMA = 'appeal-document.schema.json';
9
+ const SERVICE_USER_SCHEMA = 'service-user.schema.json';
10
+
11
+ const now = new Date().toISOString();
12
+
13
+ const BASE_HAS_CASE = {
14
+ caseId: null,
15
+ caseReference: 'mollit dolore dolor non culpa',
16
+ submissionId: null,
17
+ caseStatus: 'ready_to_start',
18
+ caseType: 'ZP',
19
+ caseProcedure: 'inquiry',
20
+ lpaCode: 'nulla',
21
+ caseOfficerId: null,
22
+ inspectorId: 'mollit in ullamco Excepteur commodo',
23
+ allocationLevel: 'D',
24
+ allocationBand: 1,
25
+ caseSpecialisms: ['et culpa elit labore', 'minim Lorem'],
26
+ caseSubmittedDate: now,
27
+ caseCreatedDate: now,
28
+ caseUpdatedDate: now,
29
+ caseValidDate: now,
30
+ caseValidationDate: null,
31
+ caseValidationOutcome: 'invalid',
32
+ caseValidationInvalidDetails: null,
33
+ caseValidationIncompleteDetails: [
34
+ 'occaecat dolor labore ut ipsum',
35
+ 'eiusmod ullamco pariatur aliqua esse',
36
+ 'eiusmod Ut officia',
37
+ 'aute',
38
+ 'do minim sint labore aliquip'
39
+ ],
40
+ caseExtensionDate: null,
41
+ caseStartedDate: null,
42
+ casePublishedDate: null,
43
+ linkedCaseStatus: null,
44
+ leadCaseReference: null,
45
+ lpaQuestionnaireDueDate: null,
46
+ lpaQuestionnaireSubmittedDate: null,
47
+ lpaQuestionnaireCreatedDate: now,
48
+ lpaQuestionnairePublishedDate: now,
49
+ lpaQuestionnaireValidationOutcome: 'incomplete',
50
+ lpaQuestionnaireValidationOutcomeDate: now,
51
+ lpaQuestionnaireValidationDetails: ['dolore cillum in anim adipisicing'],
52
+ lpaStatement: 'Ut ipsum et',
53
+ caseWithdrawnDate: null,
54
+ caseTransferredDate: null,
55
+ transferredCaseClosedDate: null,
56
+ caseDecisionOutcomeDate: null,
57
+ caseDecisionPublishedDate: null,
58
+ caseDecisionOutcome: null,
59
+ caseCompletedDate: null,
60
+ enforcementNotice: null,
61
+ applicationReference: 'eu',
62
+ applicationDate: now,
63
+ applicationDecision: 'refused',
64
+ applicationDecisionDate: now,
65
+ applicationDecisionDueDate: null,
66
+ caseSubmissionDueDate: now,
67
+ siteAddressLine1: 'ullamco',
68
+ siteAddressLine2: null,
69
+ siteAddressTown: 'dolor reprehenderit ut',
70
+ siteAddressCounty: 'dolore eu exercitation enim',
71
+ siteAddressPostcode: 'est',
72
+ siteAccessDetails: [
73
+ 'Ut',
74
+ 'eiusmod magna aliqua ipsum',
75
+ 'sit non ut amet aliqua',
76
+ 'id consectetur cupidatat',
77
+ 'incididunt ut Lorem eu'
78
+ ],
79
+ siteSafetyDetails: null,
80
+ siteAreaSquareMetres: 123,
81
+ floorSpaceSquareMetres: null,
82
+ isCorrectAppealType: null,
83
+ isGreenBelt: true,
84
+ inConservationArea: null,
85
+ affectsScheduledMonument: true,
86
+ hasProtectedSpecies: true,
87
+ isAonbNationalLandscape: null,
88
+ designatedSitesNames: null,
89
+ hasInfrastructureLevy: false,
90
+ isInfrastructureLevyFormallyAdopted: false,
91
+ infrastructureLevyAdoptedDate: null,
92
+ infrastructureLevyExpectedDate: null,
93
+ lpaProcedurePreference: null,
94
+ lpaProcedurePreferenceDetails: null,
95
+ lpaProcedurePreferenceDuration: 1,
96
+ caseworkReason: 'elit reprehenderit',
97
+ importantInformation: null,
98
+ jurisdiction: null,
99
+ redeterminedIndicator: null,
100
+ dateCostsReportDespatched: null,
101
+ dateNotRecoveredOrDerecovered: null,
102
+ dateRecovered: null,
103
+ originalCaseDecisionDate: null,
104
+ targetDate: null,
105
+ ownsAllLand: true,
106
+ ownsSomeLand: null,
107
+ knowsOtherOwners: 'Some',
108
+ knowsAllOwners: null,
109
+ advertisedAppeal: null,
110
+ notificationMethod: null,
111
+ ownersInformed: null,
112
+ originalDevelopmentDescription: null,
113
+ changedDevelopmentDescription: null,
114
+ newConditionDetails: 'tempor anim adipisicing',
115
+ nearbyCaseReferences: null,
116
+ neighbouringSiteAddresses: [
117
+ {
118
+ neighbouringSiteAddressLine1: 'eu veniam',
119
+ neighbouringSiteAddressLine2: null,
120
+ neighbouringSiteAddressTown: 'pariatur dolore',
121
+ neighbouringSiteAddressCounty: 'fugiat minim anim aliqua aliquip',
122
+ neighbouringSiteAddressPostcode: 'ex anim',
123
+ neighbouringSiteAccessDetails: null,
124
+ neighbouringSiteSafetyDetails: 'veniam in sunt'
125
+ }
126
+ ],
127
+ reasonForNeighbourVisits: null,
128
+ affectedListedBuildingNumbers: ['Excepteur', 'in consequat sit', 'esse sunt velit culpa', 'voluptate esse'],
129
+ appellantCostsAppliedFor: null,
130
+ lpaCostsAppliedFor: null,
131
+ typeOfPlanningApplication: 'householder-planning',
132
+ siteGridReferenceEasting: '357144',
133
+ siteGridReferenceNorthing: '400534',
134
+ hasLandownersPermission: null,
135
+ isSiteInAreaOfSpecialControlAdverts: false,
136
+ wasApplicationRefusedDueToHighwayOrTraffic: null,
137
+ didAppellantSubmitCompletePhotosAndPlans: null,
138
+ advertDetails: [
139
+ {
140
+ isAdvertInPosition: false,
141
+ isSiteOnHighwayLand: false,
142
+ advertType: 'Other'
143
+ }
144
+ ],
145
+ padsSapId: null
146
+ };
147
+
148
+ const BASE_S78_CASE = {
149
+ caseId: 96989394,
150
+ caseReference: 'exercitation dolore',
151
+ submissionId: null,
152
+ caseStatus: 'transferred',
153
+ caseType: 'D',
154
+ caseProcedure: 'inquiry',
155
+ lpaCode: 'pariatur cillum non',
156
+ caseOfficerId: null,
157
+ inspectorId: 'amet magna dolor in labore',
158
+ allocationLevel: null,
159
+ allocationBand: null,
160
+ caseSpecialisms: ['laboris est proident minim', 'qui sunt ut', 'consectetur Lorem do sunt aliquip'],
161
+ caseSubmittedDate: now,
162
+ caseCreatedDate: now,
163
+ caseUpdatedDate: now,
164
+ caseValidDate: null,
165
+ caseValidationDate: null,
166
+ caseValidationOutcome: null,
167
+ caseValidationInvalidDetails: null,
168
+ caseValidationIncompleteDetails: null,
169
+ caseExtensionDate: null,
170
+ caseStartedDate: now,
171
+ casePublishedDate: null,
172
+ linkedCaseStatus: 'lead',
173
+ leadCaseReference: null,
174
+ lpaQuestionnaireDueDate: null,
175
+ lpaQuestionnaireSubmittedDate: null,
176
+ lpaQuestionnaireCreatedDate: null,
177
+ lpaQuestionnairePublishedDate: null,
178
+ lpaQuestionnaireValidationOutcome: null,
179
+ lpaQuestionnaireValidationOutcomeDate: null,
180
+ lpaQuestionnaireValidationDetails: ['cupidatat dolore in', 'officia in nostrud non', 'eu amet velit labore aute'],
181
+ lpaStatement: 'ipsum consectetur labore pariatur',
182
+ listOfDocumentsBeforeDecision: null,
183
+ caseWithdrawnDate: null,
184
+ caseTransferredDate: null,
185
+ transferredCaseClosedDate: null,
186
+ caseDecisionOutcomeDate: null,
187
+ caseDecisionPublishedDate: null,
188
+ caseDecisionOutcome: null,
189
+ caseCompletedDate: null,
190
+ enforcementNotice: null,
191
+ applicationReference: 'consequat ullamco in aute amet',
192
+ applicationDate: now,
193
+ applicationDecision: 'not_received',
194
+ applicationDecisionDate: null,
195
+ applicationDecisionDueDate: now,
196
+ caseSubmissionDueDate: null,
197
+ siteAddressLine1: 'dolore mollit',
198
+ siteAddressLine2: null,
199
+ siteAddressTown: 'in',
200
+ siteAddressCounty: 'ut incididunt velit in',
201
+ siteAddressPostcode: 'ut',
202
+ siteAccessDetails: ['ullamco', 'elit in', 'velit officia pariatur mollit', 'nostrud ex culpa'],
203
+ siteSafetyDetails: null,
204
+ siteAreaSquareMetres: 69381079.34849802,
205
+ floorSpaceSquareMetres: null,
206
+ isCorrectAppealType: null,
207
+ isGreenBelt: false,
208
+ inConservationArea: null,
209
+ ownsAllLand: null,
210
+ ownsSomeLand: null,
211
+ knowsOtherOwners: null,
212
+ knowsAllOwners: null,
213
+ advertisedAppeal: true,
214
+ notificationMethod: ['notice'],
215
+ ownersInformed: null,
216
+ originalDevelopmentDescription: 'est',
217
+ changedDevelopmentDescription: null,
218
+ newConditionDetails: null,
219
+ nearbyCaseReferences: ['veniam voluptate incididunt Excepteur'],
220
+ neighbouringSiteAddresses: [
221
+ {
222
+ neighbouringSiteAddressLine1: 'laborum',
223
+ neighbouringSiteAddressLine2: null,
224
+ neighbouringSiteAddressTown: 'aute ipsum dolore',
225
+ neighbouringSiteAddressCounty: null,
226
+ neighbouringSiteAddressPostcode: 'dolor ipsum anim fugiat',
227
+ neighbouringSiteAccessDetails: 'reprehenderit mollit Duis exercitation',
228
+ neighbouringSiteSafetyDetails: null
229
+ }
230
+ ],
231
+ reasonForNeighbourVisits: null,
232
+ affectedListedBuildingNumbers: ['tempor adipisicing in'],
233
+ changedListedBuildingNumbers: null,
234
+ preserveGrantLoan: true,
235
+ consultHistoricEngland: false,
236
+ appellantCostsAppliedFor: null,
237
+ lpaCostsAppliedFor: null,
238
+ agriculturalHolding: null,
239
+ tenantAgriculturalHolding: null,
240
+ otherTenantsAgriculturalHolding: null,
241
+ informedTenantsAgriculturalHolding: true,
242
+ appellantProcedurePreference: 'hearing',
243
+ appellantProcedurePreferenceDetails: null,
244
+ appellantProcedurePreferenceDuration: null,
245
+ appellantProcedurePreferenceWitnessCount: null,
246
+ statusPlanningObligation: 'cillum proident fugiat ex quis',
247
+ affectsScheduledMonument: true,
248
+ hasProtectedSpecies: true,
249
+ isAonbNationalLandscape: null,
250
+ designatedSitesNames: null,
251
+ isGypsyOrTravellerSite: false,
252
+ isPublicRightOfWay: true,
253
+ eiaEnvironmentalImpactSchedule: 'schedule-1',
254
+ eiaDevelopmentDescription: 'agriculture-aquaculture',
255
+ eiaSensitiveAreaDetails: 'dolore non anim ut deserunt',
256
+ eiaColumnTwoThreshold: null,
257
+ eiaScreeningOpinion: true,
258
+ eiaRequiresEnvironmentalStatement: null,
259
+ eiaCompletedEnvironmentalStatement: null,
260
+ hasStatutoryConsultees: true,
261
+ consultedBodiesDetails: null,
262
+ hasInfrastructureLevy: null,
263
+ isInfrastructureLevyFormallyAdopted: null,
264
+ infrastructureLevyAdoptedDate: null,
265
+ infrastructureLevyExpectedDate: null,
266
+ lpaProcedurePreference: null,
267
+ lpaProcedurePreferenceDetails: null,
268
+ lpaProcedurePreferenceDuration: 12,
269
+ caseworkReason: 'sit ipsum',
270
+ importantInformation: null,
271
+ jurisdiction: null,
272
+ redeterminedIndicator: null,
273
+ dateCostsReportDespatched: null,
274
+ dateNotRecoveredOrDerecovered: null,
275
+ dateRecovered: null,
276
+ originalCaseDecisionDate: null,
277
+ targetDate: null,
278
+ appellantCommentsSubmittedDate: null,
279
+ appellantStatementSubmittedDate: null,
280
+ appellantProofsSubmittedDate: null,
281
+ finalCommentsDueDate: null,
282
+ interestedPartyRepsDueDate: null,
283
+ lpaCommentsSubmittedDate: null,
284
+ lpaProofsSubmittedDate: null,
285
+ lpaStatementSubmittedDate: null,
286
+ proofsOfEvidenceDueDate: null,
287
+ siteNoticesSentDate: null,
288
+ statementDueDate: null,
289
+ caseManagementConferenceDate: null,
290
+ numberOfResidencesNetChange: 59800248.893144846,
291
+ siteGridReferenceEasting: '357144',
292
+ siteGridReferenceNorthing: '400534',
293
+ siteViewableFromRoad: null,
294
+ siteWithinSSSI: null,
295
+ typeOfPlanningApplication: 'full-appeal',
296
+ developmentType: 'other-minor',
297
+ statementOfCommonGroundDueDate: null,
298
+ planningObligationDueDate: null,
299
+ advertDetails: [
300
+ {
301
+ isAdvertInPosition: false,
302
+ isSiteOnHighwayLand: false,
303
+ advertType: 'Other'
304
+ }
305
+ ],
306
+ padsSapId: null,
307
+ applicationMadeUnderActSection: 'existing-development',
308
+ siteUseAtTimeOfApplication: 'Site use at time of application',
309
+ appealUnderActSection: 'proposed-use-of-a-development',
310
+ lpaConsiderAppealInvalid: true,
311
+ lpaAppealInvalidReasons: 'Reason(s) why LPA consider the appeal invalid'
312
+ };
313
+
314
+ function randomCaseReference(prefix, index) {
315
+ const suffix = String(index + 1).padStart(7, '0');
316
+ return `${prefix}${suffix}`;
317
+ }
318
+
319
+ function randomHex(length) {
320
+ return crypto
321
+ .randomBytes(Math.ceil(length / 2))
322
+ .toString('hex')
323
+ .slice(0, length);
324
+ }
325
+
326
+ function createCase(caseType, index) {
327
+ const template = caseType === 'HAS' ? BASE_HAS_CASE : BASE_S78_CASE;
328
+ const entity = structuredClone(template);
329
+ const referencePrefix = caseType === 'HAS' ? 'HAS' : 'S78';
330
+
331
+ entity.caseId = 500000 + index;
332
+ entity.caseReference = randomCaseReference(referencePrefix, index);
333
+ entity.caseCreatedDate = now;
334
+ entity.caseUpdatedDate = now;
335
+ entity.caseSubmittedDate = now;
336
+ entity.applicationReference = `LOAD-${referencePrefix}-${index + 1}`;
337
+ entity.lpaCode = 'LOAD';
338
+
339
+ return entity;
340
+ }
341
+
342
+ function createServiceUser(casePayload, serviceUserType, index) {
343
+ const typeName = serviceUserType === 'Agent' ? 'Agent' : 'Appellant';
344
+ return {
345
+ id: crypto.randomUUID(),
346
+ salutation: 'Ms',
347
+ firstName: `Load-Test`,
348
+ lastName: `${typeName}${index + 1}`,
349
+ addressLine1: '1 Test Street',
350
+ addressLine2: null,
351
+ addressTown: 'Testville',
352
+ addressCounty: 'Testshire',
353
+ postcode: 'TE1 1ST',
354
+ addressCountry: 'United Kingdom',
355
+ organisation: 'Load Test Org',
356
+ organisationType: 'Private Company',
357
+ role: typeName,
358
+ telephoneNumber: '+447123456789',
359
+ otherPhoneNumber: null,
360
+ faxNumber: null,
361
+ emailAddress: `${typeName.toLowerCase()}${index + 1}@example.com`,
362
+ webAddress: 'https://example.com',
363
+ serviceUserType: typeName,
364
+ caseReference: casePayload.caseReference,
365
+ sourceSystem: 'back-office-appeals',
366
+ sourceSuid: crypto.randomUUID()
367
+ };
368
+ }
369
+
370
+ function createDocument(casePayload, caseIndex, documentIndex) {
371
+ const documentId = crypto.randomUUID();
372
+ const filename = `LOAD-${casePayload.caseReference}-doc-${documentIndex + 1}.pdf`;
373
+ const caseType = typeof casePayload.caseType === 'string' ? casePayload.caseType : null;
374
+ const documentUri = `https://example.local/appeal/${casePayload.caseReference}/${documentId}/v1/${filename}`;
375
+
376
+ return {
377
+ documentId,
378
+ caseId: casePayload.caseId,
379
+ caseReference: casePayload.caseReference,
380
+ version: 1,
381
+ filename,
382
+ originalFilename: filename,
383
+ size: 1000 + documentIndex,
384
+ mime: 'application/pdf',
385
+ documentURI: documentUri,
386
+ publishedDocumentURI: documentUri,
387
+ virusCheckStatus: 'scanned',
388
+ fileMD5: randomHex(32),
389
+ dateCreated: now,
390
+ dateReceived: now,
391
+ datePublished: now,
392
+ lastModified: now,
393
+ caseType,
394
+ redactedStatus: 'no_redaction_required',
395
+ documentType: 'plansDrawings',
396
+ sourceSystem: 'back-office-appeals',
397
+ origin: 'pins',
398
+ owner: 'Load Test Team',
399
+ author: 'Load Test Team',
400
+ description: `Load test document ${documentIndex + 1}`,
401
+ caseStage: 'appellant-case',
402
+ horizonFolderId: null
403
+ };
404
+ }
405
+
406
+ function buildAjvValidator() {
407
+ const schemas = loadAllSchemasSync();
408
+ const flatSchemas = Object.values(schemas).reduce((acc, current) => ({ ...acc, ...current }), {});
409
+ const ajv = new Ajv({ schemas: flatSchemas, allErrors: true });
410
+ addFormats(ajv);
411
+ return ajv;
412
+ }
413
+
414
+ function validateOrThrow(ajv, schemaName, payload, label) {
415
+ const valid = ajv.validate(schemaName, payload);
416
+ if (!valid) {
417
+ const errors = ajv.errorsText(ajv.errors, { separator: '\n' });
418
+ throw new Error(`${label} failed schema ${schemaName}:\n${errors}`);
419
+ }
420
+ }
421
+
422
+ /**
423
+ * @param {'HAS'|'S78'} [caseType]
424
+ * @param {number} [documentsPerCase]
425
+ * @param {number} [caseIndex]
426
+ * @returns {{case: any, serviceUsers: any[], documents: any[]}}
427
+ */
428
+ export const generateAppealsLoadTestMessages = (caseType = 'HAS', documentsPerCase = 2, caseIndex = 1) => {
429
+ const caseSchema = caseType === 'HAS' ? HAS_SCHEMA : S78_SCHEMA;
430
+ const ajv = buildAjvValidator();
431
+
432
+ const casePayload = createCase(caseType, caseIndex);
433
+ validateOrThrow(ajv, caseSchema, casePayload, `case[${caseIndex}]`);
434
+
435
+ const serviceUsers = [
436
+ createServiceUser(casePayload, 'Agent', caseIndex),
437
+ createServiceUser(casePayload, 'Appellant', caseIndex)
438
+ ];
439
+
440
+ for (const serviceUser of serviceUsers) {
441
+ validateOrThrow(ajv, SERVICE_USER_SCHEMA, serviceUser, `${caseIndex} serviceUser[${serviceUser.role}]`);
442
+ }
443
+
444
+ const documents = [];
445
+ for (let docIndex = 0; docIndex < documentsPerCase; docIndex += 1) {
446
+ const document = createDocument(casePayload, caseIndex, docIndex);
447
+ validateOrThrow(ajv, DOCUMENT_SCHEMA, document, `document[${caseIndex}][${docIndex}]`);
448
+ documents.push(document);
449
+ }
450
+
451
+ return {
452
+ case: casePayload,
453
+ serviceUsers,
454
+ documents
455
+ };
456
+ };
@@ -0,0 +1,58 @@
1
+ import { generateAppealsLoadTestMessages } from './generate-appeals-messages.js';
2
+ import { ServiceBusEventClient } from '../service-bus-client.js';
3
+ import { MESSAGE_EVENT_TYPE } from '../../src/enums.js';
4
+
5
+ /** @type {'HAS'|'S78'} */ //@ts-ignore
6
+ const caseType = process.env.APPEALS_LOAD_TEST_CASE_TYPE || 'HAS'; // 'HAS' or 'S78'
7
+ const caseCount = parseInt(process.env.APPEALS_LOAD_TEST_CASE_COUNT || '1', 10);
8
+ const documentsPerCase = parseInt(process.env.APPEALS_LOAD_TEST_DOCUMENTS_PER_CASE || '2', 10);
9
+ const batchSize = parseInt(process.env.APPEALS_LOAD_TEST_BATCH_SIZE || '50', 10);
10
+
11
+ if (batchSize < 1) {
12
+ throw new Error('Batch size must be at least 1');
13
+ }
14
+
15
+ const caseTypeTopicMap = {
16
+ HAS: 'appeal-has',
17
+ S78: 'appeal-s78'
18
+ };
19
+
20
+ if (!caseTypeTopicMap[caseType]) {
21
+ throw new Error(`Unsupported case type: ${caseType}`);
22
+ }
23
+
24
+ const client = new ServiceBusEventClient();
25
+
26
+ let casesCreated = 0;
27
+ let documentsCreated = 0;
28
+ let serviceUsersCreated = 0;
29
+
30
+ try {
31
+ for (let batchIndex = 0; batchIndex < caseCount; batchIndex += batchSize) {
32
+ const batch = [];
33
+ const batchEnd = Math.min(batchIndex + batchSize, caseCount);
34
+
35
+ for (let index = batchIndex; index < batchEnd; index += 1) {
36
+ batch.push(generateAppealsLoadTestMessages(caseType, documentsPerCase, index));
37
+ }
38
+
39
+ const batchCases = batch.map((m) => m.case);
40
+ const batchServiceUsers = batch.flatMap((m) => m.serviceUsers);
41
+ const batchDocuments = batch.flatMap((m) => m.documents);
42
+
43
+ await Promise.all([
44
+ client.sendEvents(caseTypeTopicMap[caseType], batchCases, MESSAGE_EVENT_TYPE.CREATE),
45
+ client.sendEvents('appeal-service-user', batchServiceUsers, MESSAGE_EVENT_TYPE.CREATE),
46
+ client.sendEvents('appeal-document', batchDocuments, MESSAGE_EVENT_TYPE.CREATE)
47
+ ]);
48
+
49
+ casesCreated += batch.length;
50
+ documentsCreated += batchDocuments.length;
51
+ serviceUsersCreated += batchServiceUsers.length;
52
+ }
53
+ } finally {
54
+ await client.close();
55
+ }
56
+
57
+ console.log('Finished');
58
+ console.log({ casesCreated, documentsCreated, serviceUsersCreated });
@@ -0,0 +1,89 @@
1
+ import { ServiceBusClient } from '@azure/service-bus';
2
+ import { DefaultAzureCredential } from '@azure/identity';
3
+
4
+ /**
5
+ * @typedef {object} ServiceBusEventClientOptions
6
+ * @property {string | undefined} [connectionString]
7
+ * @property {import('@azure/core-auth').TokenCredential | undefined} [credential]
8
+ */
9
+
10
+ /**
11
+ * Event client used to broadcast messages to service bus topics.
12
+ * @class
13
+ */
14
+ export class ServiceBusEventClient {
15
+ constructor() {
16
+ this.client = new ServiceBusClient(process.env.LOAD_TEST_SERVICE_BUS_NAMESPACE, new DefaultAzureCredential());
17
+ this.simulateSending = process.env.LOAD_TEST_SIMULATE_SENDING === 'true'; // true logs to console, false sends to service bus
18
+ }
19
+
20
+ /**
21
+ * @param {string} topic
22
+ * @returns Instance of the service bus sender from the service bus client
23
+ */
24
+ #createSender = (topic) => {
25
+ return this.client.createSender(topic);
26
+ };
27
+
28
+ /**
29
+ * Returns unix timestamp representing the trace id
30
+ * @returns {number}
31
+ */
32
+ #createTraceId = () => {
33
+ return Date.now();
34
+ };
35
+
36
+ /**
37
+ * @param {object[]} events
38
+ * @param {number} traceId
39
+ * @param {import('../utils/event-type').EventType} type
40
+ * @returns {import('@azure/service-bus').ServiceBusMessage []}
41
+ */
42
+ #transformMessagesToSend = (events, traceId, type) => {
43
+ return events.map((body) => ({
44
+ body,
45
+ contentType: 'application/json',
46
+ applicationProperties: {
47
+ version: '0.1',
48
+ traceId,
49
+ type
50
+ }
51
+ }));
52
+ };
53
+
54
+ /**
55
+ * @param {string} topic
56
+ * @param {any[]} events
57
+ * @param {import('../utils/event-type').EventType} eventType
58
+ */
59
+ sendEvents = async (topic, events, eventType) => {
60
+ const sender = this.#createSender(topic);
61
+
62
+ try {
63
+ const traceId = this.#createTraceId();
64
+
65
+ console.log(`Publishing ${events.length} events to topic ${topic} with trace id ${traceId}`);
66
+
67
+ const messages = this.#transformMessagesToSend(events, traceId, eventType);
68
+
69
+ if (this.simulateSending) {
70
+ console.log(`Simulating sending messages: ${JSON.stringify(messages)}`);
71
+ } else {
72
+ await sender.sendMessages(messages);
73
+ }
74
+
75
+ return events;
76
+ } catch (error) {
77
+ console.error(`Error sending messages to topic ${topic}:`, error);
78
+ throw error;
79
+ } finally {
80
+ if (!sender.isClosed) {
81
+ await sender.close();
82
+ }
83
+ }
84
+ };
85
+
86
+ close = async () => {
87
+ await this.client.close();
88
+ };
89
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planning-inspectorate/data-model",
3
- "version": "2.40.0",
3
+ "version": "2.42.0",
4
4
  "description": "JSON Schemas for the Planning Inspectorate's Data Model",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -16,6 +16,7 @@
16
16
  "gen-types": "node src/lib/gen-types.js",
17
17
  "gen-cjs": "npx rollup ./src/index.js --file ./src/index.cjs --format cjs && npx rollup ./src/enums.js --file ./src/enums.cjs --format cjs",
18
18
  "lint": "npx eslint .",
19
+ "load-test-appeals": "node --env-file=.env load-test/appeals/run-load-script.js",
19
20
  "format": "prettier --write .",
20
21
  "format-check": "prettier --check .",
21
22
  "prepare": "node .husky/install.mjs"
@@ -30,6 +31,8 @@
30
31
  },
31
32
  "devDependencies": {
32
33
  "@adobe/jsonschema2md": "^8.0.3",
34
+ "@azure/identity": "^4.13.0",
35
+ "@azure/service-bus": "^7.9.1",
33
36
  "@commitlint/cli": "^21.0.0",
34
37
  "@commitlint/config-conventional": "^21.0.0",
35
38
  "@eslint/js": "^10.0.1",
@@ -18,7 +18,6 @@ class InvoiceStage(Enum):
18
18
  pre_examination = "pre_examination"
19
19
  initial_examination = "initial_examination"
20
20
  final_examination = "final_examination"
21
- NoneType_None = None
22
21
 
23
22
 
24
23
  class NsipInvoice(BaseModel):
@@ -33,7 +32,7 @@ class NsipInvoice(BaseModel):
33
32
  """
34
33
  The unique identifier within the Back Office. This is not the same as the case reference
35
34
  """
36
- caseReference: str | None = Field(..., examples=["BC0510001"])
35
+ caseReference: str | None = Field(None, examples=["BC0510001"])
37
36
  invoiceStage: InvoiceStage = Field(..., examples=["pre_acceptance"])
38
37
  """
39
38
  Invoice stage
@@ -42,38 +41,38 @@ class NsipInvoice(BaseModel):
42
41
  """
43
42
  Invoice number
44
43
  """
45
- amountDue: float = Field(..., examples=[500])
44
+ amountDue: float | None = Field(None, examples=[500])
46
45
  """
47
46
  Amount due
48
47
  """
49
48
  paymentDueDate: AwareDatetime | None = Field(
50
- ..., examples=["2023-07-27T20:30:00.000Z"]
49
+ None, examples=["2023-07-27T20:30:00.000Z"]
51
50
  )
52
51
  """
53
52
  Payment due date
54
53
  """
55
54
  invoicedDate: AwareDatetime | None = Field(
56
- ..., examples=["2023-07-27T20:30:00.000Z"]
55
+ None, examples=["2023-07-27T20:30:00.000Z"]
57
56
  )
58
57
  """
59
58
  Invoiced date
60
59
  """
61
60
  paymentDate: AwareDatetime | None = Field(
62
- ..., examples=["2023-07-27T20:30:00.000Z"]
61
+ None, examples=["2023-07-27T20:30:00.000Z"]
63
62
  )
64
63
  """
65
64
  Date of payment
66
65
  """
67
- refundCreditNoteNumber: str | None = Field(..., examples=["1800004419"])
66
+ refundCreditNoteNumber: str | None = Field(None, examples=["1800004419"])
68
67
  """
69
68
  Refund credit note number
70
69
  """
71
- refundAmount: float | None = Field(..., examples=[200])
70
+ refundAmount: float | None = Field(None, examples=[200])
72
71
  """
73
72
  Amount refunded
74
73
  """
75
74
  refundIssueDate: AwareDatetime | None = Field(
76
- ..., examples=["2023-07-27T20:30:00.000Z"]
75
+ None, examples=["2023-07-27T20:30:00.000Z"]
77
76
  )
78
77
  """
79
78
  Refund issue date
@@ -5,19 +5,7 @@
5
5
  "description": "NSIP Standalone Invoice Schema for ODW Curated layer",
6
6
  "type": "object",
7
7
  "additionalProperties": true,
8
- "required": [
9
- "caseId",
10
- "caseReference",
11
- "invoiceStage",
12
- "invoiceNumber",
13
- "amountDue",
14
- "paymentDueDate",
15
- "invoicedDate",
16
- "paymentDate",
17
- "refundCreditNoteNumber",
18
- "refundAmount",
19
- "refundIssueDate"
20
- ],
8
+ "required": ["caseId", "invoiceStage", "invoiceNumber"],
21
9
  "properties": {
22
10
  "caseId": {
23
11
  "type": "integer",
@@ -29,9 +17,9 @@
29
17
  "examples": ["BC0510001"]
30
18
  },
31
19
  "invoiceStage": {
32
- "type": ["string", "null"],
20
+ "type": "string",
33
21
  "description": "Invoice stage",
34
- "enum": ["pre_acceptance", "acceptance", "pre_examination", "initial_examination", "final_examination", null],
22
+ "enum": ["pre_acceptance", "acceptance", "pre_examination", "initial_examination", "final_examination"],
35
23
  "examples": ["pre_acceptance"]
36
24
  },
37
25
  "invoiceNumber": {
@@ -40,7 +28,7 @@
40
28
  "examples": ["1800004419"]
41
29
  },
42
30
  "amountDue": {
43
- "type": "number",
31
+ "type": ["number", "null"],
44
32
  "description": "Amount due",
45
33
  "examples": [500]
46
34
  },
package/src/schemas.d.ts CHANGED
@@ -2411,17 +2411,11 @@ export interface NSIPInvoice {
2411
2411
  * The unique identifier within the Back Office. This is not the same as the case reference
2412
2412
  */
2413
2413
  caseId: number;
2414
- caseReference: string | null;
2414
+ caseReference?: string | null;
2415
2415
  /**
2416
2416
  * Invoice stage
2417
2417
  */
2418
- invoiceStage:
2419
- | 'pre_acceptance'
2420
- | 'acceptance'
2421
- | 'pre_examination'
2422
- | 'initial_examination'
2423
- | 'final_examination'
2424
- | null;
2418
+ invoiceStage: 'pre_acceptance' | 'acceptance' | 'pre_examination' | 'initial_examination' | 'final_examination';
2425
2419
  /**
2426
2420
  * Invoice number
2427
2421
  */
@@ -2429,31 +2423,31 @@ export interface NSIPInvoice {
2429
2423
  /**
2430
2424
  * Amount due
2431
2425
  */
2432
- amountDue: number;
2426
+ amountDue?: number | null;
2433
2427
  /**
2434
2428
  * Payment due date
2435
2429
  */
2436
- paymentDueDate: string | null;
2430
+ paymentDueDate?: string | null;
2437
2431
  /**
2438
2432
  * Invoiced date
2439
2433
  */
2440
- invoicedDate: string | null;
2434
+ invoicedDate?: string | null;
2441
2435
  /**
2442
2436
  * Date of payment
2443
2437
  */
2444
- paymentDate: string | null;
2438
+ paymentDate?: string | null;
2445
2439
  /**
2446
2440
  * Refund credit note number
2447
2441
  */
2448
- refundCreditNoteNumber: string | null;
2442
+ refundCreditNoteNumber?: string | null;
2449
2443
  /**
2450
2444
  * Amount refunded
2451
2445
  */
2452
- refundAmount: number | null;
2446
+ refundAmount?: number | null;
2453
2447
  /**
2454
2448
  * Refund issue date
2455
2449
  */
2456
- refundIssueDate: string | null;
2450
+ refundIssueDate?: string | null;
2457
2451
  [k: string]: unknown;
2458
2452
  }
2459
2453