@planning-inspectorate/data-model 2.56.0 → 2.58.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/docs/README.md +2 -0
- package/docs/appeal-document.md +25 -18
- package/docs/appeal-group-c.md +1420 -0
- package/docs/appeal-has.md +22 -17
- package/docs/appeal-s78.md +30 -17
- package/package.json +2 -2
- package/pins_data_model/models/model_appeal_document.py +7 -0
- package/pins_data_model/models/model_appeal_group_c.py +458 -0
- package/pins_data_model/models/model_appeal_has.py +5 -0
- package/pins_data_model/models/model_appeal_s78.py +13 -0
- package/schemas/appeal-document.schema.json +25 -1
- package/schemas/appeal-group-c.schema.json +406 -0
- package/schemas/appeal-has.schema.json +22 -1
- package/schemas/appeal-s78.schema.json +30 -1
- package/src/enums.cjs +45 -0
- package/src/enums.d.ts +43 -0
- package/src/enums.js +43 -0
- package/src/index.cjs +45 -0
- package/src/schemas.d.ts +367 -3
- package/tests/appeals/appeal-group-c.test.js +378 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import Ajv from 'ajv';
|
|
2
|
+
import addFormats from 'ajv-formats';
|
|
3
|
+
import assert from 'node:assert';
|
|
4
|
+
import { describe, it } from 'node:test';
|
|
5
|
+
import { loadAllSchemasSync } from '../../src/index.js';
|
|
6
|
+
|
|
7
|
+
const schema = 'appeal-group-c.schema.json';
|
|
8
|
+
|
|
9
|
+
const baseAppealGroupC = {
|
|
10
|
+
caseId: 600123,
|
|
11
|
+
caseReference: '6000001',
|
|
12
|
+
caseStatus: 'validation',
|
|
13
|
+
caseType: 'W',
|
|
14
|
+
caseProcedure: 'hearing',
|
|
15
|
+
lpaCode: 'Q9999',
|
|
16
|
+
caseOfficerId: 'officer-123',
|
|
17
|
+
inspectorId: 'inspector-456',
|
|
18
|
+
padsSapId: 'pads-789',
|
|
19
|
+
allocationLevel: 'A',
|
|
20
|
+
allocationBand: 1,
|
|
21
|
+
caseSpecialisms: ['Environmental', 'SSSI'],
|
|
22
|
+
caseCreatedDate: '2026-07-27T20:30:00.000Z',
|
|
23
|
+
caseSubmittedDate: '2026-07-27T20:30:00.000Z',
|
|
24
|
+
caseUpdatedDate: '2026-07-27T20:30:00.000Z',
|
|
25
|
+
caseValidDate: '2026-07-27T20:30:00.000Z',
|
|
26
|
+
caseValidationDate: '2026-07-27T20:30:00.000Z',
|
|
27
|
+
caseValidationOutcome: 'valid',
|
|
28
|
+
caseStartedDate: '2026-07-27T20:30:00.000Z',
|
|
29
|
+
casePublishedDate: '2026-07-27T20:30:00.000Z',
|
|
30
|
+
lpaQuestionnaireCreatedDate: '2026-07-27T20:30:00.000Z',
|
|
31
|
+
lpaQuestionnaireSubmittedDate: '2026-07-27T20:30:00.000Z',
|
|
32
|
+
lpaQuestionnaireValidationOutcome: 'complete',
|
|
33
|
+
lpaQuestionnaireDueDate: '2026-07-27T20:30:00.000Z',
|
|
34
|
+
caseWithdrawnDate: null,
|
|
35
|
+
caseTransferredDate: null,
|
|
36
|
+
caseDecisionOutcomeDate: null,
|
|
37
|
+
caseDecisionOutcome: null,
|
|
38
|
+
caseCompletedDate: null,
|
|
39
|
+
siteAddressLine1: '1 High Street',
|
|
40
|
+
siteAddressLine2: null,
|
|
41
|
+
siteAddressTown: 'Bristol',
|
|
42
|
+
siteAddressCounty: 'City of Bristol',
|
|
43
|
+
siteAddressPostcode: 'BS1 6PN',
|
|
44
|
+
caseManagementConferenceDate: null,
|
|
45
|
+
statementDueDate: null,
|
|
46
|
+
proofsOfEvidenceDueDate: null,
|
|
47
|
+
finalCommentsDueDate: null,
|
|
48
|
+
interestedPartyRepsDueDate: null,
|
|
49
|
+
statementOfCommonGroundDueDate: null,
|
|
50
|
+
planningObligationDueDate: null,
|
|
51
|
+
typeOfPlanningApplication: 'environmental',
|
|
52
|
+
caseSubType: 'environmental_permitting',
|
|
53
|
+
environmentalPermitSubtypes: 'flood_risk_activity',
|
|
54
|
+
enforcementNoticeReference: 'ENF-2026-01',
|
|
55
|
+
applicationReference: 'APP-2026-99',
|
|
56
|
+
linkedCaseStatus: 'lead',
|
|
57
|
+
leadCaseReference: null
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Setup Ajv instance with all schemas
|
|
62
|
+
*
|
|
63
|
+
* @returns {Ajv}
|
|
64
|
+
*/
|
|
65
|
+
function setupAjv() {
|
|
66
|
+
const ajv = new Ajv({ allErrors: true });
|
|
67
|
+
addFormats(ajv);
|
|
68
|
+
const allSchemas = loadAllSchemasSync();
|
|
69
|
+
const schemas = Object.values(allSchemas).reduce((a, c) => ({ ...a, ...c }), {});
|
|
70
|
+
for (const s of Object.values(schemas)) {
|
|
71
|
+
ajv.addSchema(s);
|
|
72
|
+
}
|
|
73
|
+
return ajv;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
describe('appeal-group-c.schema.json', () => {
|
|
77
|
+
it('should allow valid Group C appeal schema', () => {
|
|
78
|
+
const ajv = setupAjv();
|
|
79
|
+
const valid = ajv.validate(schema, structuredClone(baseAppealGroupC));
|
|
80
|
+
assert.strictEqual(valid, true, JSON.stringify(ajv.errors));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should enforce caseReference 7-digit pattern starting with 6', () => {
|
|
84
|
+
const ajv = setupAjv();
|
|
85
|
+
const testCase = structuredClone(baseAppealGroupC);
|
|
86
|
+
|
|
87
|
+
// Valid references
|
|
88
|
+
testCase.caseReference = '6000000';
|
|
89
|
+
assert.strictEqual(ajv.validate(schema, testCase), true);
|
|
90
|
+
testCase.caseReference = '6999999';
|
|
91
|
+
assert.strictEqual(ajv.validate(schema, testCase), true);
|
|
92
|
+
|
|
93
|
+
// Invalid references
|
|
94
|
+
testCase.caseReference = '7000001';
|
|
95
|
+
assert.strictEqual(ajv.validate(schema, testCase), false);
|
|
96
|
+
testCase.caseReference = '60001';
|
|
97
|
+
assert.strictEqual(ajv.validate(schema, testCase), false);
|
|
98
|
+
testCase.caseReference = '60000001';
|
|
99
|
+
assert.strictEqual(ajv.validate(schema, testCase), false);
|
|
100
|
+
testCase.caseReference = 'abcdefg';
|
|
101
|
+
assert.strictEqual(ajv.validate(schema, testCase), false);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should allow null for nullable required fields', () => {
|
|
105
|
+
const ajv = setupAjv();
|
|
106
|
+
const testCase = structuredClone(baseAppealGroupC);
|
|
107
|
+
|
|
108
|
+
testCase.caseId = null;
|
|
109
|
+
testCase.caseProcedure = null;
|
|
110
|
+
testCase.caseSubmittedDate = null;
|
|
111
|
+
testCase.caseUpdatedDate = null;
|
|
112
|
+
testCase.caseValidDate = null;
|
|
113
|
+
testCase.caseValidationDate = null;
|
|
114
|
+
testCase.caseValidationOutcome = null;
|
|
115
|
+
testCase.caseStartedDate = null;
|
|
116
|
+
testCase.casePublishedDate = null;
|
|
117
|
+
testCase.lpaQuestionnaireCreatedDate = null;
|
|
118
|
+
testCase.lpaQuestionnaireSubmittedDate = null;
|
|
119
|
+
testCase.lpaQuestionnaireValidationOutcome = null;
|
|
120
|
+
testCase.lpaQuestionnaireDueDate = null;
|
|
121
|
+
testCase.lpaCode = null;
|
|
122
|
+
testCase.caseOfficerId = null;
|
|
123
|
+
testCase.inspectorId = null;
|
|
124
|
+
testCase.padsSapId = null;
|
|
125
|
+
testCase.allocationLevel = null;
|
|
126
|
+
testCase.allocationBand = null;
|
|
127
|
+
testCase.caseSpecialisms = null;
|
|
128
|
+
testCase.siteAddressLine1 = null;
|
|
129
|
+
testCase.siteAddressLine2 = null;
|
|
130
|
+
testCase.siteAddressTown = null;
|
|
131
|
+
testCase.siteAddressCounty = null;
|
|
132
|
+
testCase.siteAddressPostcode = null;
|
|
133
|
+
|
|
134
|
+
assert.strictEqual(ajv.validate(schema, testCase), true, JSON.stringify(ajv.errors));
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should enforce required fields cannot be missing', () => {
|
|
138
|
+
const ajv = setupAjv();
|
|
139
|
+
const requiredFields = [
|
|
140
|
+
'caseId',
|
|
141
|
+
'caseReference',
|
|
142
|
+
'caseStatus',
|
|
143
|
+
'caseType',
|
|
144
|
+
'caseProcedure',
|
|
145
|
+
'lpaCode',
|
|
146
|
+
'caseOfficerId',
|
|
147
|
+
'inspectorId',
|
|
148
|
+
'caseCreatedDate'
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
for (const field of requiredFields) {
|
|
152
|
+
const testCase = structuredClone(baseAppealGroupC);
|
|
153
|
+
delete testCase[field];
|
|
154
|
+
assert.strictEqual(ajv.validate(schema, testCase), false, `Expected missing ${field} to fail`);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should allow optional fields to be omitted', () => {
|
|
159
|
+
const ajv = setupAjv();
|
|
160
|
+
const testCase = structuredClone(baseAppealGroupC);
|
|
161
|
+
|
|
162
|
+
delete testCase.caseManagementConferenceDate;
|
|
163
|
+
delete testCase.statementDueDate;
|
|
164
|
+
delete testCase.proofsOfEvidenceDueDate;
|
|
165
|
+
delete testCase.finalCommentsDueDate;
|
|
166
|
+
delete testCase.interestedPartyRepsDueDate;
|
|
167
|
+
delete testCase.statementOfCommonGroundDueDate;
|
|
168
|
+
delete testCase.planningObligationDueDate;
|
|
169
|
+
delete testCase.typeOfPlanningApplication;
|
|
170
|
+
delete testCase.caseSubType;
|
|
171
|
+
delete testCase.environmentalPermitSubtypes;
|
|
172
|
+
delete testCase.enforcementNoticeReference;
|
|
173
|
+
delete testCase.applicationReference;
|
|
174
|
+
delete testCase.linkedCaseStatus;
|
|
175
|
+
delete testCase.leadCaseReference;
|
|
176
|
+
|
|
177
|
+
assert.strictEqual(ajv.validate(schema, testCase), true, JSON.stringify(ajv.errors));
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should validate decision outcomes including new Group C outcomes', () => {
|
|
181
|
+
const ajv = setupAjv();
|
|
182
|
+
const testCase = structuredClone(baseAppealGroupC);
|
|
183
|
+
|
|
184
|
+
const validOutcomes = [
|
|
185
|
+
'allowed',
|
|
186
|
+
'split_decision',
|
|
187
|
+
'dismissed',
|
|
188
|
+
'invalid',
|
|
189
|
+
'notice_upheld',
|
|
190
|
+
'planning_permission_granted',
|
|
191
|
+
'quashed_on_legal_grounds',
|
|
192
|
+
'notice_varied_and_upheld',
|
|
193
|
+
'notice_quashed',
|
|
194
|
+
null
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
for (const outcome of validOutcomes) {
|
|
198
|
+
testCase.caseDecisionOutcome = outcome;
|
|
199
|
+
assert.strictEqual(ajv.validate(schema, testCase), true, `Outcome ${outcome} should be valid`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
testCase.caseDecisionOutcome = 'unknown_outcome';
|
|
203
|
+
assert.strictEqual(ajv.validate(schema, testCase), false);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should validate typeOfPlanningApplication enums', () => {
|
|
207
|
+
const ajv = setupAjv();
|
|
208
|
+
const testCase = structuredClone(baseAppealGroupC);
|
|
209
|
+
|
|
210
|
+
const validTypes = [
|
|
211
|
+
'full-appeal',
|
|
212
|
+
'householder-planning',
|
|
213
|
+
'outline-planning',
|
|
214
|
+
'prior-approval',
|
|
215
|
+
'reserved-matters',
|
|
216
|
+
'removal-or-variation-of-conditions',
|
|
217
|
+
'listed-building',
|
|
218
|
+
'minor-commercial-development',
|
|
219
|
+
'advertisement',
|
|
220
|
+
'lawful-development-certificate',
|
|
221
|
+
'discontinuance-notice',
|
|
222
|
+
'permission-in-principle',
|
|
223
|
+
'tree-preservation-order',
|
|
224
|
+
'tree-replacement-notice',
|
|
225
|
+
'high-hedge-appeal',
|
|
226
|
+
'hedgerows-appeal',
|
|
227
|
+
'community-infrastructure-levy',
|
|
228
|
+
'planning-obligation',
|
|
229
|
+
'environmental',
|
|
230
|
+
null
|
|
231
|
+
];
|
|
232
|
+
|
|
233
|
+
for (const type of validTypes) {
|
|
234
|
+
testCase.typeOfPlanningApplication = type;
|
|
235
|
+
assert.strictEqual(ajv.validate(schema, testCase), true, `typeOfPlanningApplication ${type} should be valid`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
testCase.typeOfPlanningApplication = 'invalid_planning_app_type';
|
|
239
|
+
assert.strictEqual(ajv.validate(schema, testCase), false);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should validate caseSubType enums', () => {
|
|
243
|
+
const ajv = setupAjv();
|
|
244
|
+
const testCase = structuredClone(baseAppealGroupC);
|
|
245
|
+
|
|
246
|
+
const validSubTypes = [
|
|
247
|
+
'anti_pollution_works_notice',
|
|
248
|
+
'contaminated_land',
|
|
249
|
+
'marine_licensing',
|
|
250
|
+
'waste_carriers',
|
|
251
|
+
'water_abstraction',
|
|
252
|
+
'environmental_liability',
|
|
253
|
+
'environmental_permitting',
|
|
254
|
+
'hazardous_substances',
|
|
255
|
+
'silage_and_slurry',
|
|
256
|
+
'sssi',
|
|
257
|
+
'affordable_housing',
|
|
258
|
+
null
|
|
259
|
+
];
|
|
260
|
+
|
|
261
|
+
for (const subType of validSubTypes) {
|
|
262
|
+
testCase.caseSubType = subType;
|
|
263
|
+
assert.strictEqual(ajv.validate(schema, testCase), true, `SubType ${subType} should be valid`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
testCase.caseSubType = 'invalid_sub_type';
|
|
267
|
+
assert.strictEqual(ajv.validate(schema, testCase), false);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should validate environmentalPermitSubtypes enums', () => {
|
|
271
|
+
const ajv = setupAjv();
|
|
272
|
+
const testCase = structuredClone(baseAppealGroupC);
|
|
273
|
+
|
|
274
|
+
const validPermitSubtypes = [
|
|
275
|
+
'refusal_of_a_permit',
|
|
276
|
+
'deemed_refusal',
|
|
277
|
+
'conditions_attached_to_a_permit',
|
|
278
|
+
'decision_to_impose_conditions_by_regulator',
|
|
279
|
+
'deemed_withdrawal',
|
|
280
|
+
'enforcement_notice',
|
|
281
|
+
'suspension_notice',
|
|
282
|
+
'closure_notice',
|
|
283
|
+
'flood_risk_activity',
|
|
284
|
+
'revocation_notice',
|
|
285
|
+
'prohibition_notice',
|
|
286
|
+
null
|
|
287
|
+
];
|
|
288
|
+
|
|
289
|
+
for (const permitType of validPermitSubtypes) {
|
|
290
|
+
testCase.environmentalPermitSubtypes = permitType;
|
|
291
|
+
assert.strictEqual(ajv.validate(schema, testCase), true, `Permit subtype ${permitType} should be valid`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
testCase.environmentalPermitSubtypes = 'invalid_permit';
|
|
295
|
+
assert.strictEqual(ajv.validate(schema, testCase), false);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('should allow additional properties', () => {
|
|
299
|
+
const ajv = setupAjv();
|
|
300
|
+
const testCase = structuredClone(baseAppealGroupC);
|
|
301
|
+
testCase.customExtraProperty = 'extraValue';
|
|
302
|
+
assert.strictEqual(ajv.validate(schema, testCase), true);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('should allow appellantCase and lpaQuestionnaire in appeal-document schema', () => {
|
|
306
|
+
const ajv = setupAjv();
|
|
307
|
+
const docSchema = 'appeal-document.schema.json';
|
|
308
|
+
|
|
309
|
+
const baseDoc = {
|
|
310
|
+
documentId: 'doc-1',
|
|
311
|
+
caseId: 600123,
|
|
312
|
+
caseReference: '6000001',
|
|
313
|
+
version: 1,
|
|
314
|
+
filename: 'appeal-form.pdf',
|
|
315
|
+
originalFilename: 'appeal-form.pdf',
|
|
316
|
+
size: 1024,
|
|
317
|
+
mime: 'application/pdf',
|
|
318
|
+
documentURI: 'https://example.com/doc.pdf',
|
|
319
|
+
publishedDocumentURI: null,
|
|
320
|
+
virusCheckStatus: 'scanned',
|
|
321
|
+
fileMD5: 'hash123',
|
|
322
|
+
dateCreated: '2026-07-27T20:30:00.000Z',
|
|
323
|
+
dateReceived: '2026-07-27T20:30:00.000Z',
|
|
324
|
+
datePublished: null,
|
|
325
|
+
lastModified: '2026-07-27T20:30:00.000Z',
|
|
326
|
+
caseType: 'W',
|
|
327
|
+
redactedStatus: 'not_redacted',
|
|
328
|
+
sourceSystem: 'back-office-appeals',
|
|
329
|
+
origin: 'citizen',
|
|
330
|
+
owner: 'appellant',
|
|
331
|
+
author: 'appellant',
|
|
332
|
+
description: 'Appeal submission document',
|
|
333
|
+
caseStage: 'appellant-case',
|
|
334
|
+
horizonFolderId: null,
|
|
335
|
+
documentType: 'appellantCase'
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
assert.strictEqual(ajv.validate(docSchema, baseDoc), true, JSON.stringify(ajv.errors));
|
|
339
|
+
baseDoc.documentType = 'lpaQuestionnaire';
|
|
340
|
+
assert.strictEqual(ajv.validate(docSchema, baseDoc), true, JSON.stringify(ajv.errors));
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('should validate caseType enums including new letter codes', () => {
|
|
344
|
+
const ajv = setupAjv();
|
|
345
|
+
const testCase = structuredClone(baseAppealGroupC);
|
|
346
|
+
|
|
347
|
+
const validCaseTypes = [
|
|
348
|
+
'C',
|
|
349
|
+
'D',
|
|
350
|
+
'F',
|
|
351
|
+
'G',
|
|
352
|
+
'H',
|
|
353
|
+
'L',
|
|
354
|
+
'Q',
|
|
355
|
+
'S',
|
|
356
|
+
'V',
|
|
357
|
+
'W',
|
|
358
|
+
'X',
|
|
359
|
+
'Y',
|
|
360
|
+
'Z',
|
|
361
|
+
'ZA',
|
|
362
|
+
'ZP',
|
|
363
|
+
'TPO',
|
|
364
|
+
'TRN',
|
|
365
|
+
'HH',
|
|
366
|
+
'HR',
|
|
367
|
+
'ENV'
|
|
368
|
+
];
|
|
369
|
+
|
|
370
|
+
for (const caseType of validCaseTypes) {
|
|
371
|
+
testCase.caseType = caseType;
|
|
372
|
+
assert.strictEqual(ajv.validate(schema, testCase), true, `caseType ${caseType} should be valid`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
testCase.caseType = 'INVALID_CODE';
|
|
376
|
+
assert.strictEqual(ajv.validate(schema, testCase), false);
|
|
377
|
+
});
|
|
378
|
+
});
|