@pdtf/schemas 3.4.0-9 → 3.4.1-2

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.
Files changed (29) hide show
  1. package/.claude/settings.local.json +15 -0
  2. package/README.md +326 -6
  3. package/index.js +131 -0
  4. package/package.json +1 -1
  5. package/src/examples/v3/exampleTransaction.json +5 -1
  6. package/src/schemas/v3/combined.json +8 -21
  7. package/src/schemas/v3/overlays/README.md +299 -0
  8. package/src/schemas/v3/overlays/extensions/as.json +57 -0
  9. package/src/schemas/v3/overlays/extensions/dr.json +60 -0
  10. package/src/schemas/v3/overlays/extensions/er.json +71 -0
  11. package/src/schemas/v3/overlays/extensions/fd.json +63 -0
  12. package/src/schemas/v3/overlays/extensions/hi.json +44 -0
  13. package/src/schemas/v3/overlays/extensions/hs.json +56 -0
  14. package/src/schemas/v3/overlays/extensions/jk.json +55 -0
  15. package/src/schemas/v3/overlays/extensions/la.json +66 -0
  16. package/src/schemas/v3/overlays/extensions/ma.json +87 -0
  17. package/src/schemas/v3/overlays/extensions/mc.json +36 -0
  18. package/src/schemas/v3/overlays/extensions/oa.json +22 -0
  19. package/src/schemas/v3/overlays/extensions/oc.json +35 -0
  20. package/src/schemas/v3/overlays/extensions/sb.json +56 -0
  21. package/src/schemas/v3/overlays/extensions/sf.json +57 -0
  22. package/src/schemas/v3/overlays/extensions/sl.json +46 -0
  23. package/src/schemas/v3/overlays/extensions/tf.json +91 -0
  24. package/src/schemas/v3/overlays/nts2.json +6 -18
  25. package/src/schemas/v3/overlays/ntsl2.json +2 -8
  26. package/src/schemas/v3/overlays/ta6.json +1 -0
  27. package/src/schemas/v3/property-transaction-milestones.json +91 -0
  28. package/src/tests/v3/extensionOverlays.test.js +397 -0
  29. package/src/utils/extractExtensionOverlays.js +318 -0
@@ -0,0 +1,91 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "Transaction Milestones",
4
+ "description": "Schema for tracking transaction milestones throughout the conveyancing process",
5
+ "type": "object",
6
+ "properties": {
7
+ "listed": {
8
+ "type": "object",
9
+ "properties": {
10
+ "completed": {
11
+ "type": "string",
12
+ "format": "date-time"
13
+ }
14
+ }
15
+ },
16
+ "legalForms": {
17
+ "type": "object",
18
+ "properties": {
19
+ "completed": {
20
+ "type": "string",
21
+ "format": "date-time"
22
+ }
23
+ }
24
+ },
25
+ "soldSubjectToContract": {
26
+ "type": "object",
27
+ "properties": {
28
+ "completed": {
29
+ "type": "string",
30
+ "format": "date-time"
31
+ }
32
+ }
33
+ },
34
+ "searches": {
35
+ "type": "object",
36
+ "properties": {
37
+ "ordered": {
38
+ "type": "string",
39
+ "format": "date-time"
40
+ },
41
+ "expected": {
42
+ "type": "string",
43
+ "format": "date-time"
44
+ },
45
+ "completed": {
46
+ "type": "string",
47
+ "format": "date-time"
48
+ }
49
+ }
50
+ },
51
+ "enquiries": {
52
+ "type": "object",
53
+ "properties": {
54
+ "completed": {
55
+ "type": "string",
56
+ "format": "date-time"
57
+ }
58
+ }
59
+ },
60
+ "exchangeOfContracts": {
61
+ "type": "object",
62
+ "properties": {
63
+ "expected": {
64
+ "type": "string",
65
+ "format": "date-time"
66
+ },
67
+ "completed": {
68
+ "type": "string",
69
+ "format": "date-time"
70
+ }
71
+ }
72
+ },
73
+ "completion": {
74
+ "type": "object",
75
+ "properties": {
76
+ "started": {
77
+ "type": "string",
78
+ "format": "date-time"
79
+ },
80
+ "expected": {
81
+ "type": "string",
82
+ "format": "date-time"
83
+ },
84
+ "completed": {
85
+ "type": "string",
86
+ "format": "date-time"
87
+ }
88
+ }
89
+ }
90
+ }
91
+ }
@@ -0,0 +1,397 @@
1
+ const {
2
+ getTransactionSchema,
3
+ extensionOverlays,
4
+ getValidator,
5
+ } = require("../../../index.js");
6
+
7
+ const schemaId =
8
+ "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json";
9
+
10
+ describe("Extension Overlays", () => {
11
+ describe("String key support", () => {
12
+ test("should load extension overlays using string keys", () => {
13
+ const schema = getTransactionSchema(schemaId, ["nts2023", "jk"]);
14
+ const jkProp =
15
+ schema.properties?.propertyPack?.properties?.specialistIssues
16
+ ?.properties?.japaneseKnotweed;
17
+
18
+ expect(jkProp).toBeDefined();
19
+ expect(jkProp.ntsRef).toBe("A5.3");
20
+ });
21
+
22
+ test("should work with multiple extension string keys", () => {
23
+ const schema = getTransactionSchema(schemaId, [
24
+ "nts2023",
25
+ "jk",
26
+ "tf",
27
+ "ma",
28
+ "er",
29
+ ]);
30
+
31
+ const jkProp =
32
+ schema.properties?.propertyPack?.properties?.specialistIssues
33
+ ?.properties?.japaneseKnotweed;
34
+ const tfProp =
35
+ schema.properties?.propertyPack?.properties?.ownership?.properties
36
+ ?.ownershipsToBeTransferred?.items?.oneOf?.[2]?.properties
37
+ ?.leaseholdInformation?.properties?.serviceCharge?.oneOf?.[1]
38
+ ?.properties?.transferFees;
39
+ const maProp =
40
+ schema.properties?.propertyPack?.properties?.ownership?.properties
41
+ ?.ownershipsToBeTransferred?.items?.oneOf?.[2]?.properties
42
+ ?.leaseholdInformation?.properties?.contactDetails;
43
+ const erProp =
44
+ schema.properties?.propertyPack?.properties?.ownership?.properties
45
+ ?.ownershipsToBeTransferred?.items?.oneOf?.[0]?.properties
46
+ ?.estateRentcharges;
47
+
48
+ expect(jkProp?.ntsRef).toBe("A5.3");
49
+ expect(tfProp?.ntsRef).toBe("A3.5.1.1");
50
+ expect(maProp?.ntsRef).toBe("A1.5.4");
51
+ expect(erProp?.ntsRef).toBe("A3.4");
52
+ });
53
+
54
+ test("should be equivalent to direct object access", () => {
55
+ const schemaString = getTransactionSchema(schemaId, ["nts2023", "jk"]);
56
+ const schemaDirect = getTransactionSchema(schemaId, [
57
+ "nts2023",
58
+ extensionOverlays.jk,
59
+ ]);
60
+
61
+ expect(schemaString).toEqual(schemaDirect);
62
+ });
63
+ });
64
+
65
+ describe("Individual extension functionality", () => {
66
+ test("should add Japanese Knotweed extension correctly", () => {
67
+ const schema = getTransactionSchema(schemaId, ["nts2023", "jk"]);
68
+ const jkProp =
69
+ schema.properties?.propertyPack?.properties?.specialistIssues
70
+ ?.properties?.japaneseKnotweed;
71
+
72
+ expect(jkProp).toBeDefined();
73
+ expect(jkProp.ntsRef).toBe("A5.3");
74
+ expect(jkProp.required).toContain("yesNo");
75
+ expect(jkProp.discriminator?.propertyName).toBe("yesNo");
76
+ });
77
+
78
+ test("should add Transfer Fees extension correctly", () => {
79
+ const schema = getTransactionSchema(schemaId, ["nts2023", "tf"]);
80
+ const tfProp =
81
+ schema.properties?.propertyPack?.properties?.ownership?.properties
82
+ ?.ownershipsToBeTransferred?.items?.oneOf?.[2]?.properties
83
+ ?.leaseholdInformation?.properties?.serviceCharge?.oneOf?.[1]
84
+ ?.properties?.transferFees;
85
+
86
+ expect(tfProp).toBeDefined();
87
+ expect(tfProp.ntsRef).toBe("A3.5.1.1");
88
+ expect(tfProp.title).toBe(
89
+ "Are there any additional fees payable on sale or letting?"
90
+ );
91
+ });
92
+
93
+ test("should add Managing Agent extension correctly", () => {
94
+ const schema = getTransactionSchema(schemaId, ["nts2023", "ma"]);
95
+ const maProp =
96
+ schema.properties?.propertyPack?.properties?.ownership?.properties
97
+ ?.ownershipsToBeTransferred?.items?.oneOf?.[2]?.properties
98
+ ?.leaseholdInformation?.properties?.contactDetails;
99
+
100
+ expect(maProp).toBeDefined();
101
+ expect(maProp.ntsRef).toBe("A1.5.4");
102
+ expect(maProp.required).toContain("contacts");
103
+ });
104
+
105
+ test("should add Solar Panels extension correctly", () => {
106
+ const schema = getTransactionSchema(schemaId, ["nts2023", "sl"]);
107
+ const slProp =
108
+ schema.properties?.propertyPack?.properties?.electricity?.properties
109
+ ?.solarPanels?.oneOf?.[1]?.properties?.panelsOwnedOutright;
110
+
111
+ expect(slProp).toBeDefined();
112
+ expect(slProp.ntsRef).toBe("B3.7.1");
113
+ });
114
+
115
+ test("should add Heating Installation extension correctly", () => {
116
+ const schema = getTransactionSchema(schemaId, ["nts2023", "hi"]);
117
+ const hiProp =
118
+ schema.properties?.propertyPack?.properties?.heating?.properties
119
+ ?.heatingSystem?.oneOf?.[1]?.properties?.centralHeatingDetails
120
+ ?.properties?.centralHeatingInstalled;
121
+
122
+ expect(hiProp).toBeDefined();
123
+ expect(hiProp.ntsRef).toBe("B3.4.3.2");
124
+ });
125
+
126
+ test("should add Estate Rentcharges extension correctly", () => {
127
+ const schema = getTransactionSchema(schemaId, ["nts2023", "er"]);
128
+ const erProp =
129
+ schema.properties?.propertyPack?.properties?.ownership?.properties
130
+ ?.ownershipsToBeTransferred?.items?.oneOf?.[0]?.properties
131
+ ?.estateRentcharges;
132
+
133
+ expect(erProp).toBeDefined();
134
+ expect(erProp.ntsRef).toBe("A3.4");
135
+ expect(erProp.required).toContain("yesNo");
136
+ expect(erProp.discriminator?.propertyName).toBe("yesNo");
137
+ expect(erProp.oneOf).toBeDefined();
138
+ expect(Array.isArray(erProp.oneOf)).toBe(true);
139
+ expect(erProp.oneOf.length).toBeGreaterThan(0);
140
+
141
+ // Check the conditional branch for "Yes" response
142
+ const yesBranch = erProp.oneOf.find(
143
+ (branch) =>
144
+ branch.required?.includes("details") &&
145
+ branch.required?.includes("amount")
146
+ );
147
+ expect(yesBranch).toBeDefined();
148
+ expect(yesBranch.properties?.details?.ntsRef).toBe("A3.4.2");
149
+ expect(yesBranch.properties?.amount?.ntsRef).toBe("A3.4.3");
150
+ });
151
+ });
152
+
153
+ describe("Extension overlay availability", () => {
154
+ test("should export all extension overlays", () => {
155
+ const expectedKeys = [
156
+ "as",
157
+ "dr",
158
+ "jk",
159
+ "sb",
160
+ "hs", // Specialist Issues
161
+ "oa",
162
+ "la",
163
+ "sf",
164
+ "mc", // Property Features
165
+ "er",
166
+ "ma",
167
+ "tf", // Ownership & Financial
168
+ "sl",
169
+ "hi",
170
+ "fd", // Utilities & Services
171
+ "oc", // Transaction
172
+ ];
173
+
174
+ expect(Object.keys(extensionOverlays).sort()).toEqual(
175
+ expectedKeys.sort()
176
+ );
177
+ });
178
+
179
+ test("should have valid JSON structure for all extensions", () => {
180
+ Object.entries(extensionOverlays).forEach(([key, overlay]) => {
181
+ expect(overlay).toBeDefined();
182
+ expect(overlay.$schema).toBe("http://json-schema.org/draft-07/schema#");
183
+ expect(overlay.$id).toContain(`extensions/${key}.json`);
184
+ expect(overlay.properties).toBeDefined();
185
+ });
186
+ });
187
+ });
188
+
189
+ describe("NTS2 equivalence test", () => {
190
+ test("should produce equivalent result to nts2 when all relevant extensions are combined", () => {
191
+ // Get the full NTS2 schema
192
+ const nts2Schema = getTransactionSchema(schemaId, ["nts2025"]);
193
+
194
+ // Get NTS base with all relevant extensions
195
+ const allExtensions = [
196
+ "nts2023",
197
+ // Specialist issues extensions
198
+ "as",
199
+ "dr",
200
+ "jk",
201
+ "sb",
202
+ "hs",
203
+ // Property features extensions
204
+ "oa",
205
+ "la",
206
+ "sf",
207
+ "mc",
208
+ // Ownership & financial extensions
209
+ "er",
210
+ "ma",
211
+ "tf",
212
+ // Utilities & services extensions
213
+ "sl",
214
+ "hi",
215
+ "fd",
216
+ // Transaction extensions
217
+ "oc",
218
+ ];
219
+
220
+ const ntsWithAllExtensions = getTransactionSchema(
221
+ schemaId,
222
+ allExtensions
223
+ );
224
+
225
+ // Compare key structural elements
226
+ const nts2PropPack = nts2Schema.properties?.propertyPack;
227
+ const extPropPack = ntsWithAllExtensions.properties?.propertyPack;
228
+
229
+ // Check that specialist issues section exists in both
230
+ expect(nts2PropPack?.properties?.specialistIssues).toBeDefined();
231
+ expect(extPropPack?.properties?.specialistIssues).toBeDefined();
232
+
233
+ // Check key specialist issues properties
234
+ const nts2SI = nts2PropPack.properties.specialistIssues.properties;
235
+ const extSI = extPropPack.properties.specialistIssues.properties;
236
+
237
+ expect(extSI?.japaneseKnotweed).toBeDefined();
238
+ expect(extSI?.dryRotEtcTreatment).toBeDefined();
239
+ expect(extSI?.containsAsbestos).toBeDefined();
240
+ expect(extSI?.subsidenceOrStructuralFault).toBeDefined();
241
+ expect(extSI?.ongoingHealthOrSafetyIssue).toBeDefined();
242
+
243
+ // Check that references match (extensions use ntsRef, NTS2 uses nts2Ref)
244
+ if (nts2SI?.japaneseKnotweed && extSI?.japaneseKnotweed) {
245
+ expect(extSI.japaneseKnotweed.ntsRef).toBe("A5.3");
246
+ // NTS2 might have nts2Ref instead - this is expected difference
247
+ }
248
+
249
+ // Check outside areas
250
+ expect(
251
+ extPropPack?.properties?.residentialPropertyFeatures?.properties
252
+ ?.outsideAreas
253
+ ).toBeDefined();
254
+
255
+ // Check leasehold extensions
256
+ const leaseholdInfo =
257
+ extPropPack?.properties?.ownership?.properties
258
+ ?.ownershipsToBeTransferred?.items?.oneOf?.[2]?.properties
259
+ ?.leaseholdInformation;
260
+ expect(leaseholdInfo?.properties?.contactDetails).toBeDefined(); // Managing agent
261
+ expect(
262
+ leaseholdInfo?.properties?.serviceCharge?.oneOf?.[1]?.properties
263
+ ?.transferFees
264
+ ).toBeDefined(); // Transfer fees
265
+
266
+ // Check utility extensions
267
+ expect(
268
+ extPropPack?.properties?.electricity?.properties?.solarPanels
269
+ ?.oneOf?.[1]?.properties?.panelsOwnedOutright
270
+ ).toBeDefined();
271
+ expect(
272
+ extPropPack?.properties?.heating?.properties?.heatingSystem?.oneOf?.[1]
273
+ ?.properties?.centralHeatingDetails?.properties
274
+ ?.centralHeatingInstalled
275
+ ).toBeDefined();
276
+
277
+ // Check construction extensions
278
+ expect(
279
+ extPropPack?.properties?.typeOfConstruction?.properties?.loft
280
+ ).toBeDefined();
281
+ expect(
282
+ extPropPack?.properties?.typeOfConstruction?.properties
283
+ ?.sprayFoamInsulation
284
+ ).toBeDefined();
285
+
286
+ // Check completion and moving
287
+ expect(
288
+ extPropPack?.properties?.completionAndMoving?.properties
289
+ ?.otherPropertyInChain
290
+ ).toBeDefined();
291
+ });
292
+
293
+ test("should validate data successfully with all extensions", () => {
294
+ const allExtensions = [
295
+ "nts2023",
296
+ "as",
297
+ "dr",
298
+ "jk",
299
+ "sb",
300
+ "hs",
301
+ "oa",
302
+ "la",
303
+ "sf",
304
+ "mc",
305
+ "er",
306
+ "ma",
307
+ "tf",
308
+ "sl",
309
+ "hi",
310
+ "fd",
311
+ "oc",
312
+ ];
313
+
314
+ const validator = getValidator(schemaId, allExtensions);
315
+ expect(validator).toBeDefined();
316
+ expect(typeof validator).toBe("function");
317
+
318
+ // Test with minimal valid data structure
319
+ const testData = {
320
+ propertyPack: {
321
+ priceInformation: { price: 100000, priceQualifier: "Freehold" },
322
+ ownership: { ownershipsToBeTransferred: [] },
323
+ councilTax: { band: "A" },
324
+ energyEfficiency: { epcRating: "C" },
325
+ buildInformation: { building: { buildDate: 2000 } },
326
+ typeOfConstruction: { isStandardForm: { yesNo: "Yes" } },
327
+ electricity: { mainsElectricity: { yesNo: "Yes" } },
328
+ connectivity: { mobilePhoneCoverage: { signal: "Good" } },
329
+ waterAndDrainage: { water: { supplier: "Thames Water" } },
330
+ heating: { heatingSystem: { yesNo: "Yes" } },
331
+ parking: { parkingArrangements: { yesNo: "No" } },
332
+ listingAndConservation: { listedBuilding: { yesNo: "No" } },
333
+ rightsAndInformalArrangements: { rightsGranted: { yesNo: "No" } },
334
+ environmentalIssues: { flooding: { yesNo: "No" } },
335
+ notices: { noticesAndProposals: { yesNo: "No" } },
336
+ specialistIssues: {
337
+ japaneseKnotweed: { yesNo: "No" },
338
+ dryRotEtcTreatment: { yesNo: "No" },
339
+ containsAsbestos: { yesNo: "No" },
340
+ subsidenceOrStructuralFault: { yesNo: "No" },
341
+ ongoingHealthOrSafetyIssue: { yesNo: "No" },
342
+ },
343
+ },
344
+ };
345
+
346
+ // This might not validate due to missing required fields, but validator should not throw
347
+ expect(() => validator(testData)).not.toThrow();
348
+ });
349
+ });
350
+
351
+ describe("Extension merging behavior", () => {
352
+ test("should preserve required arrays when merging extensions", () => {
353
+ const schema = getTransactionSchema(schemaId, ["nts2023", "jk", "as"]);
354
+ const siSection =
355
+ schema.properties?.propertyPack?.properties?.specialistIssues;
356
+
357
+ expect(siSection?.required).toBeDefined();
358
+ expect(siSection.required).toContain("japaneseKnotweed");
359
+ expect(siSection.required).toContain("containsAsbestos");
360
+ });
361
+
362
+ test("should handle discriminator patterns correctly", () => {
363
+ const schema = getTransactionSchema(schemaId, ["nts2023", "jk"]);
364
+ const jkProp =
365
+ schema.properties?.propertyPack?.properties?.specialistIssues
366
+ ?.properties?.japaneseKnotweed;
367
+
368
+ expect(jkProp?.discriminator).toBeDefined();
369
+ expect(jkProp.discriminator.propertyName).toBe("yesNo");
370
+ expect(jkProp.oneOf).toBeDefined();
371
+ expect(Array.isArray(jkProp.oneOf)).toBe(true);
372
+ });
373
+
374
+ test("should merge oneOf schemas correctly", () => {
375
+ const schema = getTransactionSchema(schemaId, ["nts2023", "tf"]);
376
+ const tfProp =
377
+ schema.properties?.propertyPack?.properties?.ownership?.properties
378
+ ?.ownershipsToBeTransferred?.items?.oneOf?.[2]?.properties
379
+ ?.leaseholdInformation?.properties?.serviceCharge?.oneOf?.[1]
380
+ ?.properties?.transferFees;
381
+
382
+ expect(tfProp?.oneOf).toBeDefined();
383
+ expect(tfProp.oneOf.length).toBeGreaterThan(0);
384
+
385
+ // Check the structure - details should be in the conditional oneOf branch
386
+ const conditionalBranch = tfProp.oneOf.find(
387
+ (branch) =>
388
+ branch.properties?.details || branch.required?.includes("details")
389
+ );
390
+ expect(conditionalBranch).toBeDefined();
391
+ expect(
392
+ conditionalBranch.properties?.details ||
393
+ conditionalBranch.required?.includes("details")
394
+ ).toBeTruthy();
395
+ });
396
+ });
397
+ });