@pdtf/schemas 3.4.1-1 → 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.
package/index.js CHANGED
@@ -22,23 +22,23 @@ const extensionOverlays = {
22
22
  jk: require("./src/schemas/v3/overlays/extensions/jk.json"),
23
23
  sb: require("./src/schemas/v3/overlays/extensions/sb.json"),
24
24
  hs: require("./src/schemas/v3/overlays/extensions/hs.json"),
25
-
25
+
26
26
  // Property Features Extensions
27
27
  oa: require("./src/schemas/v3/overlays/extensions/oa.json"),
28
28
  la: require("./src/schemas/v3/overlays/extensions/la.json"),
29
29
  sf: require("./src/schemas/v3/overlays/extensions/sf.json"),
30
30
  mc: require("./src/schemas/v3/overlays/extensions/mc.json"),
31
-
31
+
32
32
  // Ownership & Financial Extensions
33
33
  er: require("./src/schemas/v3/overlays/extensions/er.json"),
34
34
  ma: require("./src/schemas/v3/overlays/extensions/ma.json"),
35
35
  tf: require("./src/schemas/v3/overlays/extensions/tf.json"),
36
-
36
+
37
37
  // Utilities & Services Extensions
38
38
  sl: require("./src/schemas/v3/overlays/extensions/sl.json"),
39
39
  hi: require("./src/schemas/v3/overlays/extensions/hi.json"),
40
40
  fd: require("./src/schemas/v3/overlays/extensions/fd.json"),
41
-
41
+
42
42
  // Transaction Extensions
43
43
  oc: require("./src/schemas/v3/overlays/extensions/oc.json"),
44
44
  };
@@ -165,10 +165,109 @@ const getTransactionSchema = (
165
165
  },
166
166
  arrayMerge: combineMerge,
167
167
  });
168
+
169
+ // Post-merge: Ensure overlay metadata is preserved
170
+ mergedSchema = preserveOverlayMetadata(mergedSchema, overlaySchema);
168
171
  });
169
172
  return mergedSchema;
170
173
  };
171
174
 
175
+ // Function to ensure overlay metadata properties are preserved after merging
176
+ const preserveOverlayMetadata = (mergedSchema, overlaySchema) => {
177
+ const metadataKeys = [
178
+ "ntsRef",
179
+ "nts2Ref",
180
+ "baspi4Ref",
181
+ "baspi5Ref",
182
+ "ta6Ref",
183
+ "ta7Ref",
184
+ "ta10Ref",
185
+ "lpe1Ref",
186
+ "fme1Ref",
187
+ "con29RRef",
188
+ "con29DWRef",
189
+ "llc1Ref",
190
+ "rdsRef",
191
+ "oc1Ref",
192
+ "piqRef",
193
+ "sr24Ref",
194
+ "discriminator",
195
+ ];
196
+
197
+ // Helper to get a unique key signature for a schema branch (for matching oneOf branches)
198
+ const getBranchSignature = (obj) => {
199
+ if (!obj || typeof obj !== "object") return "";
200
+ if (obj.properties) {
201
+ return Object.keys(obj.properties).sort().join(",");
202
+ }
203
+ return Object.keys(obj).sort().join(",");
204
+ };
205
+
206
+ const traverseAndPreserve = (merged, overlay, path = "") => {
207
+ if (!overlay || typeof overlay !== "object") return;
208
+
209
+ // If both are arrays, try to match by signature, else fallback to index
210
+ if (Array.isArray(overlay) && Array.isArray(merged)) {
211
+ // Build a map of overlay signatures
212
+ const overlaySignatures = overlay.map(getBranchSignature);
213
+ const mergedSignatures = merged.map(getBranchSignature);
214
+ for (let i = 0; i < overlay.length; i++) {
215
+ const oSig = overlaySignatures[i];
216
+ // Try to find a matching branch in merged by signature
217
+ let mIdx = mergedSignatures.indexOf(oSig);
218
+ if (mIdx === -1) mIdx = i; // fallback to index if not found
219
+ if (merged[mIdx] !== undefined) {
220
+ traverseAndPreserve(merged[mIdx], overlay[i], path + `[${mIdx}]`);
221
+ }
222
+ }
223
+ return;
224
+ }
225
+
226
+ Object.keys(overlay).forEach((key) => {
227
+ const overlayValue = overlay[key];
228
+ const mergedValue = merged[key];
229
+
230
+ if (metadataKeys.includes(key) && overlayValue !== undefined) {
231
+ // For metadata keys, preserve the overlay value
232
+ merged[key] = overlayValue;
233
+ } else if (
234
+ key === "required" &&
235
+ Array.isArray(overlayValue) &&
236
+ Array.isArray(mergedValue)
237
+ ) {
238
+ // For required arrays, merge them uniquely (don't overwrite)
239
+ merged[key] = [...new Set([...mergedValue, ...overlayValue])];
240
+ } else if (
241
+ key === "required" &&
242
+ Array.isArray(overlayValue) &&
243
+ !Array.isArray(mergedValue)
244
+ ) {
245
+ // If merged doesn't have required array but overlay does, use overlay's
246
+ merged[key] = overlayValue;
247
+ } else if (
248
+ overlayValue &&
249
+ typeof overlayValue === "object" &&
250
+ !Array.isArray(overlayValue) &&
251
+ mergedValue &&
252
+ typeof mergedValue === "object" &&
253
+ !Array.isArray(mergedValue)
254
+ ) {
255
+ traverseAndPreserve(mergedValue, overlayValue, path + "/" + key);
256
+ } else if (
257
+ Array.isArray(overlayValue) &&
258
+ Array.isArray(mergedValue) &&
259
+ ["oneOf", "anyOf", "allOf"].includes(key)
260
+ ) {
261
+ // Recursively handle schema composition arrays (handled above)
262
+ traverseAndPreserve(mergedValue, overlayValue, path + `/${key}`);
263
+ }
264
+ });
265
+ };
266
+
267
+ traverseAndPreserve(mergedSchema, overlaySchema);
268
+ return mergedSchema;
269
+ };
270
+
172
271
  // Add helper function to generate cache key
173
272
  const generateOverlayKey = (overlays) => {
174
273
  if (!overlays) return "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pdtf/schemas",
3
- "version": "3.4.1-1",
3
+ "version": "3.4.1-2",
4
4
  "description": "Property Data Trust Framework Schemas and Utilities",
5
5
  "main": "index.js",
6
6
  "directories": {
@@ -18,7 +18,11 @@
18
18
  "countryCode": "GBR"
19
19
  },
20
20
  "role": "Seller",
21
- "sellersCapacity": { "capacity": "Legal Owner" },
21
+ "sellersCapacity": {
22
+ "capacity": "Legal Owner",
23
+ "sellersCapacityDetails": "Selling as the legal owner of the property",
24
+ "attachments": "To follow"
25
+ },
22
26
  "externalIds": { "Moverly": "123434" }
23
27
  },
24
28
  {
@@ -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
+ }
@@ -4,35 +4,60 @@ const {
4
4
  getValidator,
5
5
  } = require("../../../index.js");
6
6
 
7
- const schemaId = "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json";
7
+ const schemaId =
8
+ "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json";
8
9
 
9
10
  describe("Extension Overlays", () => {
10
-
11
11
  describe("String key support", () => {
12
12
  test("should load extension overlays using string keys", () => {
13
13
  const schema = getTransactionSchema(schemaId, ["nts2023", "jk"]);
14
- const jkProp = schema.properties?.propertyPack?.properties?.specialistIssues?.properties?.japaneseKnotweed;
15
-
14
+ const jkProp =
15
+ schema.properties?.propertyPack?.properties?.specialistIssues
16
+ ?.properties?.japaneseKnotweed;
17
+
16
18
  expect(jkProp).toBeDefined();
17
19
  expect(jkProp.ntsRef).toBe("A5.3");
18
20
  });
19
21
 
20
22
  test("should work with multiple extension string keys", () => {
21
- const schema = getTransactionSchema(schemaId, ["nts2023", "jk", "tf", "ma"]);
22
-
23
- const jkProp = schema.properties?.propertyPack?.properties?.specialistIssues?.properties?.japaneseKnotweed;
24
- const tfProp = schema.properties?.propertyPack?.properties?.ownership?.properties?.ownershipsToBeTransferred?.items?.oneOf?.[2]?.properties?.leaseholdInformation?.properties?.serviceCharge?.oneOf?.[1]?.properties?.transferFees;
25
- const maProp = schema.properties?.propertyPack?.properties?.ownership?.properties?.ownershipsToBeTransferred?.items?.oneOf?.[2]?.properties?.leaseholdInformation?.properties?.contactDetails;
26
-
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
+
27
48
  expect(jkProp?.ntsRef).toBe("A5.3");
28
49
  expect(tfProp?.ntsRef).toBe("A3.5.1.1");
29
50
  expect(maProp?.ntsRef).toBe("A1.5.4");
51
+ expect(erProp?.ntsRef).toBe("A3.4");
30
52
  });
31
53
 
32
54
  test("should be equivalent to direct object access", () => {
33
55
  const schemaString = getTransactionSchema(schemaId, ["nts2023", "jk"]);
34
- const schemaDirect = getTransactionSchema(schemaId, ["nts2023", extensionOverlays.jk]);
35
-
56
+ const schemaDirect = getTransactionSchema(schemaId, [
57
+ "nts2023",
58
+ extensionOverlays.jk,
59
+ ]);
60
+
36
61
  expect(schemaString).toEqual(schemaDirect);
37
62
  });
38
63
  });
@@ -40,8 +65,10 @@ describe("Extension Overlays", () => {
40
65
  describe("Individual extension functionality", () => {
41
66
  test("should add Japanese Knotweed extension correctly", () => {
42
67
  const schema = getTransactionSchema(schemaId, ["nts2023", "jk"]);
43
- const jkProp = schema.properties?.propertyPack?.properties?.specialistIssues?.properties?.japaneseKnotweed;
44
-
68
+ const jkProp =
69
+ schema.properties?.propertyPack?.properties?.specialistIssues
70
+ ?.properties?.japaneseKnotweed;
71
+
45
72
  expect(jkProp).toBeDefined();
46
73
  expect(jkProp.ntsRef).toBe("A5.3");
47
74
  expect(jkProp.required).toContain("yesNo");
@@ -50,17 +77,26 @@ describe("Extension Overlays", () => {
50
77
 
51
78
  test("should add Transfer Fees extension correctly", () => {
52
79
  const schema = getTransactionSchema(schemaId, ["nts2023", "tf"]);
53
- const tfProp = schema.properties?.propertyPack?.properties?.ownership?.properties?.ownershipsToBeTransferred?.items?.oneOf?.[2]?.properties?.leaseholdInformation?.properties?.serviceCharge?.oneOf?.[1]?.properties?.transferFees;
54
-
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
+
55
86
  expect(tfProp).toBeDefined();
56
87
  expect(tfProp.ntsRef).toBe("A3.5.1.1");
57
- expect(tfProp.title).toBe("Are there any additional fees payable on sale or letting?");
88
+ expect(tfProp.title).toBe(
89
+ "Are there any additional fees payable on sale or letting?"
90
+ );
58
91
  });
59
92
 
60
93
  test("should add Managing Agent extension correctly", () => {
61
94
  const schema = getTransactionSchema(schemaId, ["nts2023", "ma"]);
62
- const maProp = schema.properties?.propertyPack?.properties?.ownership?.properties?.ownershipsToBeTransferred?.items?.oneOf?.[2]?.properties?.leaseholdInformation?.properties?.contactDetails;
63
-
95
+ const maProp =
96
+ schema.properties?.propertyPack?.properties?.ownership?.properties
97
+ ?.ownershipsToBeTransferred?.items?.oneOf?.[2]?.properties
98
+ ?.leaseholdInformation?.properties?.contactDetails;
99
+
64
100
  expect(maProp).toBeDefined();
65
101
  expect(maProp.ntsRef).toBe("A1.5.4");
66
102
  expect(maProp.required).toContain("contacts");
@@ -68,32 +104,76 @@ describe("Extension Overlays", () => {
68
104
 
69
105
  test("should add Solar Panels extension correctly", () => {
70
106
  const schema = getTransactionSchema(schemaId, ["nts2023", "sl"]);
71
- const slProp = schema.properties?.propertyPack?.properties?.electricity?.properties?.solarPanels?.oneOf?.[1]?.properties?.panelsOwnedOutright;
72
-
107
+ const slProp =
108
+ schema.properties?.propertyPack?.properties?.electricity?.properties
109
+ ?.solarPanels?.oneOf?.[1]?.properties?.panelsOwnedOutright;
110
+
73
111
  expect(slProp).toBeDefined();
74
112
  expect(slProp.ntsRef).toBe("B3.7.1");
75
113
  });
76
114
 
77
115
  test("should add Heating Installation extension correctly", () => {
78
116
  const schema = getTransactionSchema(schemaId, ["nts2023", "hi"]);
79
- const hiProp = schema.properties?.propertyPack?.properties?.heating?.properties?.heatingSystem?.oneOf?.[1]?.properties?.centralHeatingDetails?.properties?.centralHeatingInstalled;
80
-
117
+ const hiProp =
118
+ schema.properties?.propertyPack?.properties?.heating?.properties
119
+ ?.heatingSystem?.oneOf?.[1]?.properties?.centralHeatingDetails
120
+ ?.properties?.centralHeatingInstalled;
121
+
81
122
  expect(hiProp).toBeDefined();
82
123
  expect(hiProp.ntsRef).toBe("B3.4.3.2");
83
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
+ });
84
151
  });
85
152
 
86
153
  describe("Extension overlay availability", () => {
87
154
  test("should export all extension overlays", () => {
88
155
  const expectedKeys = [
89
- "as", "dr", "jk", "sb", "hs", // Specialist Issues
90
- "oa", "la", "sf", "mc", // Property Features
91
- "er", "ma", "tf", // Ownership & Financial
92
- "sl", "hi", "fd", // Utilities & Services
93
- "oc" // Transaction
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
94
172
  ];
95
-
96
- expect(Object.keys(extensionOverlays).sort()).toEqual(expectedKeys.sort());
173
+
174
+ expect(Object.keys(extensionOverlays).sort()).toEqual(
175
+ expectedKeys.sort()
176
+ );
97
177
  });
98
178
 
99
179
  test("should have valid JSON structure for all extensions", () => {
@@ -110,78 +190,131 @@ describe("Extension Overlays", () => {
110
190
  test("should produce equivalent result to nts2 when all relevant extensions are combined", () => {
111
191
  // Get the full NTS2 schema
112
192
  const nts2Schema = getTransactionSchema(schemaId, ["nts2025"]);
113
-
193
+
114
194
  // Get NTS base with all relevant extensions
115
195
  const allExtensions = [
116
196
  "nts2023",
117
197
  // Specialist issues extensions
118
- "as", "dr", "jk", "sb", "hs",
119
- // Property features extensions
120
- "oa", "la", "sf", "mc",
198
+ "as",
199
+ "dr",
200
+ "jk",
201
+ "sb",
202
+ "hs",
203
+ // Property features extensions
204
+ "oa",
205
+ "la",
206
+ "sf",
207
+ "mc",
121
208
  // Ownership & financial extensions
122
- "er", "ma", "tf",
209
+ "er",
210
+ "ma",
211
+ "tf",
123
212
  // Utilities & services extensions
124
- "sl", "hi", "fd",
213
+ "sl",
214
+ "hi",
215
+ "fd",
125
216
  // Transaction extensions
126
- "oc"
217
+ "oc",
127
218
  ];
128
-
129
- const ntsWithAllExtensions = getTransactionSchema(schemaId, allExtensions);
130
-
219
+
220
+ const ntsWithAllExtensions = getTransactionSchema(
221
+ schemaId,
222
+ allExtensions
223
+ );
224
+
131
225
  // Compare key structural elements
132
226
  const nts2PropPack = nts2Schema.properties?.propertyPack;
133
227
  const extPropPack = ntsWithAllExtensions.properties?.propertyPack;
134
-
228
+
135
229
  // Check that specialist issues section exists in both
136
230
  expect(nts2PropPack?.properties?.specialistIssues).toBeDefined();
137
231
  expect(extPropPack?.properties?.specialistIssues).toBeDefined();
138
-
232
+
139
233
  // Check key specialist issues properties
140
234
  const nts2SI = nts2PropPack.properties.specialistIssues.properties;
141
235
  const extSI = extPropPack.properties.specialistIssues.properties;
142
-
236
+
143
237
  expect(extSI?.japaneseKnotweed).toBeDefined();
144
238
  expect(extSI?.dryRotEtcTreatment).toBeDefined();
145
239
  expect(extSI?.containsAsbestos).toBeDefined();
146
240
  expect(extSI?.subsidenceOrStructuralFault).toBeDefined();
147
241
  expect(extSI?.ongoingHealthOrSafetyIssue).toBeDefined();
148
-
242
+
149
243
  // Check that references match (extensions use ntsRef, NTS2 uses nts2Ref)
150
244
  if (nts2SI?.japaneseKnotweed && extSI?.japaneseKnotweed) {
151
245
  expect(extSI.japaneseKnotweed.ntsRef).toBe("A5.3");
152
246
  // NTS2 might have nts2Ref instead - this is expected difference
153
247
  }
154
-
248
+
155
249
  // Check outside areas
156
- expect(extPropPack?.properties?.residentialPropertyFeatures?.properties?.outsideAreas).toBeDefined();
157
-
250
+ expect(
251
+ extPropPack?.properties?.residentialPropertyFeatures?.properties
252
+ ?.outsideAreas
253
+ ).toBeDefined();
254
+
158
255
  // Check leasehold extensions
159
- const leaseholdInfo = extPropPack?.properties?.ownership?.properties?.ownershipsToBeTransferred?.items?.oneOf?.[2]?.properties?.leaseholdInformation;
256
+ const leaseholdInfo =
257
+ extPropPack?.properties?.ownership?.properties
258
+ ?.ownershipsToBeTransferred?.items?.oneOf?.[2]?.properties
259
+ ?.leaseholdInformation;
160
260
  expect(leaseholdInfo?.properties?.contactDetails).toBeDefined(); // Managing agent
161
- expect(leaseholdInfo?.properties?.serviceCharge?.oneOf?.[1]?.properties?.transferFees).toBeDefined(); // Transfer fees
162
-
261
+ expect(
262
+ leaseholdInfo?.properties?.serviceCharge?.oneOf?.[1]?.properties
263
+ ?.transferFees
264
+ ).toBeDefined(); // Transfer fees
265
+
163
266
  // Check utility extensions
164
- expect(extPropPack?.properties?.electricity?.properties?.solarPanels?.oneOf?.[1]?.properties?.panelsOwnedOutright).toBeDefined();
165
- expect(extPropPack?.properties?.heating?.properties?.heatingSystem?.oneOf?.[1]?.properties?.centralHeatingDetails?.properties?.centralHeatingInstalled).toBeDefined();
166
-
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
+
167
277
  // Check construction extensions
168
- expect(extPropPack?.properties?.typeOfConstruction?.properties?.loft).toBeDefined();
169
- expect(extPropPack?.properties?.typeOfConstruction?.properties?.sprayFoamInsulation).toBeDefined();
170
-
278
+ expect(
279
+ extPropPack?.properties?.typeOfConstruction?.properties?.loft
280
+ ).toBeDefined();
281
+ expect(
282
+ extPropPack?.properties?.typeOfConstruction?.properties
283
+ ?.sprayFoamInsulation
284
+ ).toBeDefined();
285
+
171
286
  // Check completion and moving
172
- expect(extPropPack?.properties?.completionAndMoving?.properties?.otherPropertyInChain).toBeDefined();
287
+ expect(
288
+ extPropPack?.properties?.completionAndMoving?.properties
289
+ ?.otherPropertyInChain
290
+ ).toBeDefined();
173
291
  });
174
292
 
175
293
  test("should validate data successfully with all extensions", () => {
176
294
  const allExtensions = [
177
- "nts2023", "as", "dr", "jk", "sb", "hs", "oa", "la", "sf", "mc",
178
- "er", "ma", "tf", "sl", "hi", "fd", "oc"
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",
179
312
  ];
180
-
313
+
181
314
  const validator = getValidator(schemaId, allExtensions);
182
315
  expect(validator).toBeDefined();
183
316
  expect(typeof validator).toBe("function");
184
-
317
+
185
318
  // Test with minimal valid data structure
186
319
  const testData = {
187
320
  propertyPack: {
@@ -205,11 +338,11 @@ describe("Extension Overlays", () => {
205
338
  dryRotEtcTreatment: { yesNo: "No" },
206
339
  containsAsbestos: { yesNo: "No" },
207
340
  subsidenceOrStructuralFault: { yesNo: "No" },
208
- ongoingHealthOrSafetyIssue: { yesNo: "No" }
209
- }
210
- }
341
+ ongoingHealthOrSafetyIssue: { yesNo: "No" },
342
+ },
343
+ },
211
344
  };
212
-
345
+
213
346
  // This might not validate due to missing required fields, but validator should not throw
214
347
  expect(() => validator(testData)).not.toThrow();
215
348
  });
@@ -218,8 +351,9 @@ describe("Extension Overlays", () => {
218
351
  describe("Extension merging behavior", () => {
219
352
  test("should preserve required arrays when merging extensions", () => {
220
353
  const schema = getTransactionSchema(schemaId, ["nts2023", "jk", "as"]);
221
- const siSection = schema.properties?.propertyPack?.properties?.specialistIssues;
222
-
354
+ const siSection =
355
+ schema.properties?.propertyPack?.properties?.specialistIssues;
356
+
223
357
  expect(siSection?.required).toBeDefined();
224
358
  expect(siSection.required).toContain("japaneseKnotweed");
225
359
  expect(siSection.required).toContain("containsAsbestos");
@@ -227,8 +361,10 @@ describe("Extension Overlays", () => {
227
361
 
228
362
  test("should handle discriminator patterns correctly", () => {
229
363
  const schema = getTransactionSchema(schemaId, ["nts2023", "jk"]);
230
- const jkProp = schema.properties?.propertyPack?.properties?.specialistIssues?.properties?.japaneseKnotweed;
231
-
364
+ const jkProp =
365
+ schema.properties?.propertyPack?.properties?.specialistIssues
366
+ ?.properties?.japaneseKnotweed;
367
+
232
368
  expect(jkProp?.discriminator).toBeDefined();
233
369
  expect(jkProp.discriminator.propertyName).toBe("yesNo");
234
370
  expect(jkProp.oneOf).toBeDefined();
@@ -237,17 +373,25 @@ describe("Extension Overlays", () => {
237
373
 
238
374
  test("should merge oneOf schemas correctly", () => {
239
375
  const schema = getTransactionSchema(schemaId, ["nts2023", "tf"]);
240
- const tfProp = schema.properties?.propertyPack?.properties?.ownership?.properties?.ownershipsToBeTransferred?.items?.oneOf?.[2]?.properties?.leaseholdInformation?.properties?.serviceCharge?.oneOf?.[1]?.properties?.transferFees;
241
-
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
+
242
382
  expect(tfProp?.oneOf).toBeDefined();
243
383
  expect(tfProp.oneOf.length).toBeGreaterThan(0);
244
-
384
+
245
385
  // Check the structure - details should be in the conditional oneOf branch
246
- const conditionalBranch = tfProp.oneOf.find(branch =>
247
- branch.properties?.details || branch.required?.includes("details")
386
+ const conditionalBranch = tfProp.oneOf.find(
387
+ (branch) =>
388
+ branch.properties?.details || branch.required?.includes("details")
248
389
  );
249
390
  expect(conditionalBranch).toBeDefined();
250
- expect(conditionalBranch.properties?.details || conditionalBranch.required?.includes("details")).toBeTruthy();
391
+ expect(
392
+ conditionalBranch.properties?.details ||
393
+ conditionalBranch.required?.includes("details")
394
+ ).toBeTruthy();
251
395
  });
252
396
  });
253
- });
397
+ });
@@ -11,120 +11,152 @@ const extensionMappings = {
11
11
  oa: {
12
12
  name: "outsideAreas",
13
13
  description: "Outside areas extension for NTS",
14
- paths: ["/properties/propertyPack/properties/residentialPropertyFeatures/properties/outsideAreas"]
14
+ paths: [
15
+ "/properties/propertyPack/properties/residentialPropertyFeatures/properties/outsideAreas",
16
+ ],
15
17
  },
16
-
18
+
17
19
  // Estate rentcharges for freehold
18
20
  er: {
19
21
  name: "estateRentcharges",
20
22
  description: "Estate rentcharges for freehold properties",
21
- paths: ["/properties/propertyPack/properties/ownership/properties/ownershipsToBeTransferred/items/oneOf/0/properties/estateRentcharges"]
23
+ paths: [
24
+ "/properties/propertyPack/properties/ownership/properties/ownershipsToBeTransferred/items/oneOf/0/properties/estateRentcharges",
25
+ ],
22
26
  },
23
-
27
+
24
28
  // Managing agent contact details for leasehold
25
29
  ma: {
26
30
  name: "managingAgent",
27
31
  description: "Managing agent contact details for leasehold",
28
- paths: ["/properties/propertyPack/properties/ownership/properties/ownershipsToBeTransferred/items/oneOf/2/properties/leaseholdInformation/properties/contactDetails"]
32
+ paths: [
33
+ "/properties/propertyPack/properties/ownership/properties/ownershipsToBeTransferred/items/oneOf/2/properties/leaseholdInformation/properties/contactDetails",
34
+ ],
29
35
  },
30
-
36
+
31
37
  // Transfer fees for leasehold
32
38
  tf: {
33
39
  name: "transferFees",
34
40
  description: "Transfer fees for leasehold",
35
- paths: ["/properties/propertyPack/properties/ownership/properties/ownershipsToBeTransferred/items/oneOf/2/properties/leaseholdInformation/properties/serviceCharge/oneOf/1/properties/transferFees"]
41
+ paths: [
42
+ "/properties/propertyPack/properties/ownership/properties/ownershipsToBeTransferred/items/oneOf/2/properties/leaseholdInformation/properties/serviceCharge/oneOf/1/properties/transferFees",
43
+ ],
36
44
  },
37
-
45
+
38
46
  // Main construction type if standard
39
47
  mc: {
40
48
  name: "mainConstruction",
41
49
  description: "Main construction type if standard form",
42
- paths: ["/properties/propertyPack/properties/typeOfConstruction/properties/isStandardForm/oneOf/0/properties/constructionType"]
50
+ paths: [
51
+ "/properties/propertyPack/properties/typeOfConstruction/properties/isStandardForm/oneOf/0/properties/constructionType",
52
+ ],
43
53
  },
44
-
54
+
45
55
  // Loft access and details
46
56
  la: {
47
57
  name: "loftAccess",
48
58
  description: "Loft access and details",
49
- paths: ["/properties/propertyPack/properties/typeOfConstruction/properties/loft"]
59
+ paths: [
60
+ "/properties/propertyPack/properties/typeOfConstruction/properties/loft",
61
+ ],
50
62
  },
51
-
63
+
52
64
  // Spray foam insulation
53
65
  sf: {
54
66
  name: "sprayFoam",
55
67
  description: "Spray foam insulation",
56
- paths: ["/properties/propertyPack/properties/typeOfConstruction/properties/sprayFoamInsulation"]
68
+ paths: [
69
+ "/properties/propertyPack/properties/typeOfConstruction/properties/sprayFoamInsulation",
70
+ ],
57
71
  },
58
-
72
+
59
73
  // Specialist issues - dry rot
60
74
  dr: {
61
75
  name: "dryRot",
62
76
  description: "Dry rot treatment specialist issue",
63
- paths: ["/properties/propertyPack/properties/specialistIssues/properties/dryRotEtcTreatment"]
77
+ paths: [
78
+ "/properties/propertyPack/properties/specialistIssues/properties/dryRotEtcTreatment",
79
+ ],
64
80
  },
65
-
81
+
66
82
  // Specialist issues - asbestos
67
83
  as: {
68
84
  name: "asbestos",
69
85
  description: "Asbestos specialist issue",
70
- paths: ["/properties/propertyPack/properties/specialistIssues/properties/containsAsbestos"]
86
+ paths: [
87
+ "/properties/propertyPack/properties/specialistIssues/properties/containsAsbestos",
88
+ ],
71
89
  },
72
-
90
+
73
91
  // Specialist issues - Japanese knotweed
74
92
  jk: {
75
93
  name: "japaneseKnotweed",
76
94
  description: "Japanese knotweed specialist issue",
77
- paths: ["/properties/propertyPack/properties/specialistIssues/properties/japaneseKnotweed"]
95
+ paths: [
96
+ "/properties/propertyPack/properties/specialistIssues/properties/japaneseKnotweed",
97
+ ],
78
98
  },
79
-
99
+
80
100
  // Specialist issues - subsidence
81
101
  sb: {
82
102
  name: "subsidence",
83
103
  description: "Subsidence or structural fault specialist issue",
84
- paths: ["/properties/propertyPack/properties/specialistIssues/properties/subsidenceOrStructuralFault"]
104
+ paths: [
105
+ "/properties/propertyPack/properties/specialistIssues/properties/subsidenceOrStructuralFault",
106
+ ],
85
107
  },
86
-
108
+
87
109
  // Specialist issues - health and safety
88
110
  hs: {
89
111
  name: "healthSafety",
90
112
  description: "Health and safety specialist issue",
91
- paths: ["/properties/propertyPack/properties/specialistIssues/properties/ongoingHealthOrSafetyIssue"]
113
+ paths: [
114
+ "/properties/propertyPack/properties/specialistIssues/properties/ongoingHealthOrSafetyIssue",
115
+ ],
92
116
  },
93
-
117
+
94
118
  // Solar panels leased
95
119
  sl: {
96
120
  name: "solarPanelsLeased",
97
121
  description: "Solar panels ownership details",
98
- paths: ["/properties/propertyPack/properties/electricity/properties/solarPanels/oneOf/1/properties/panelsOwnedOutright"]
122
+ paths: [
123
+ "/properties/propertyPack/properties/electricity/properties/solarPanels/oneOf/1/properties/panelsOwnedOutright",
124
+ ],
99
125
  },
100
-
126
+
101
127
  // Heating installation date
102
128
  hi: {
103
129
  name: "heatingInstalled",
104
130
  description: "Central heating installation date",
105
- paths: ["/properties/propertyPack/properties/heating/properties/heatingSystem/oneOf/1/properties/centralHeatingDetails/properties/centralHeatingInstalled"]
131
+ paths: [
132
+ "/properties/propertyPack/properties/heating/properties/heatingSystem/oneOf/1/properties/centralHeatingDetails/properties/centralHeatingInstalled",
133
+ ],
106
134
  },
107
-
135
+
108
136
  // Flood defences
109
137
  fd: {
110
138
  name: "floodDefences",
111
139
  description: "Flood defence information",
112
- paths: ["/properties/propertyPack/properties/environmentalIssues/properties/flooding/properties/floodDefences"]
140
+ paths: [
141
+ "/properties/propertyPack/properties/environmentalIssues/properties/flooding/properties/floodDefences",
142
+ ],
113
143
  },
114
-
144
+
115
145
  // Other property in chain
116
146
  oc: {
117
147
  name: "otherPropertyChain",
118
148
  description: "Other property in chain dependency",
119
- paths: ["/properties/propertyPack/properties/completionAndMoving/properties/otherPropertyInChain"]
120
- }
149
+ paths: [
150
+ "/properties/propertyPack/properties/completionAndMoving/properties/otherPropertyInChain",
151
+ ],
152
+ },
121
153
  };
122
154
 
123
155
  // Function to extract properties at specific paths with nts2Ref
124
156
  function extractPathsWithNts2Ref(schema, paths) {
125
157
  const result = {};
126
-
127
- paths.forEach(path => {
158
+
159
+ paths.forEach((path) => {
128
160
  try {
129
161
  const value = jp.get(schema, path);
130
162
  if (value && hasNts2Ref(value)) {
@@ -135,14 +167,14 @@ function extractPathsWithNts2Ref(schema, paths) {
135
167
  console.warn(`Path not found: ${path}`);
136
168
  }
137
169
  });
138
-
170
+
139
171
  return result;
140
172
  }
141
173
 
142
174
  // Check if an object or its descendants have nts2Ref
143
175
  function hasNts2Ref(obj) {
144
176
  let found = false;
145
- traverse(obj).forEach(function(element) {
177
+ traverse(obj).forEach(function (element) {
146
178
  if (element && element.nts2Ref) {
147
179
  found = true;
148
180
  this.stop();
@@ -154,23 +186,23 @@ function hasNts2Ref(obj) {
154
186
  // Extract only NTS2-specific properties (with nts2Ref)
155
187
  function extractNts2Properties(obj) {
156
188
  const result = {};
157
-
189
+
158
190
  // Copy nts2-specific metadata at current level
159
191
  if (obj.nts2Ref) result.ntsRef = obj.nts2Ref;
160
192
  if (obj.nts2Required) result.required = obj.nts2Required;
161
193
  if (obj.nts2Title) result.title = obj.nts2Title;
162
194
  if (obj.nts2Description) result.description = obj.nts2Description;
163
195
  if (obj.nts2Enum) result.enum = obj.nts2Enum;
164
-
196
+
165
197
  // Handle discriminator
166
198
  if (obj.discriminator) {
167
199
  result.discriminator = obj.discriminator;
168
200
  }
169
-
201
+
170
202
  // Recursively process properties
171
203
  if (obj.properties) {
172
204
  result.properties = {};
173
- Object.keys(obj.properties).forEach(key => {
205
+ Object.keys(obj.properties).forEach((key) => {
174
206
  const prop = obj.properties[key];
175
207
  if (hasNts2Ref(prop)) {
176
208
  result.properties[key] = extractNts2Properties(prop);
@@ -181,14 +213,14 @@ function extractNts2Properties(obj) {
181
213
  delete result.properties;
182
214
  }
183
215
  }
184
-
216
+
185
217
  // Handle arrays
186
218
  if (obj.items) {
187
219
  if (hasNts2Ref(obj.items)) {
188
220
  result.items = extractNts2Properties(obj.items);
189
221
  }
190
222
  }
191
-
223
+
192
224
  // Handle oneOf
193
225
  if (obj.oneOf) {
194
226
  const processedOneOf = [];
@@ -201,7 +233,9 @@ function extractNts2Properties(obj) {
201
233
  if (schema.properties[propName]) {
202
234
  if (!extracted.properties) extracted.properties = {};
203
235
  extracted.properties[propName] = {
204
- enum: schema.properties[propName].nts2Enum || schema.properties[propName].enum
236
+ enum:
237
+ schema.properties[propName].nts2Enum ||
238
+ schema.properties[propName].enum,
205
239
  };
206
240
  }
207
241
  }
@@ -212,20 +246,29 @@ function extractNts2Properties(obj) {
212
246
  result.oneOf = processedOneOf;
213
247
  }
214
248
  }
215
-
249
+
216
250
  // Copy other JSON Schema keywords that might be present
217
- ['type', 'format', 'minimum', 'maximum', 'minLength', 'maxLength', 'minItems', 'maxItems'].forEach(keyword => {
251
+ [
252
+ "type",
253
+ "format",
254
+ "minimum",
255
+ "maximum",
256
+ "minLength",
257
+ "maxLength",
258
+ "minItems",
259
+ "maxItems",
260
+ ].forEach((keyword) => {
218
261
  if (obj[keyword] !== undefined) {
219
262
  result[keyword] = obj[keyword];
220
263
  }
221
264
  });
222
-
265
+
223
266
  return result;
224
267
  }
225
268
 
226
269
  // Add required array at parent levels if needed
227
270
  function addRequiredArrays(overlay, schema) {
228
- traverse(overlay).forEach(function(node) {
271
+ traverse(overlay).forEach(function (node) {
229
272
  if (node && node.properties) {
230
273
  const schemaPath = "/" + this.path.join("/");
231
274
  try {
@@ -233,7 +276,9 @@ function addRequiredArrays(overlay, schema) {
233
276
  if (schemaNode && schemaNode.nts2Required) {
234
277
  // Filter to only include properties that exist in this overlay
235
278
  const overlayProps = Object.keys(node.properties);
236
- const requiredProps = schemaNode.nts2Required.filter(prop => overlayProps.includes(prop));
279
+ const requiredProps = schemaNode.nts2Required.filter((prop) =>
280
+ overlayProps.includes(prop)
281
+ );
237
282
  if (requiredProps.length > 0) {
238
283
  node.required = requiredProps;
239
284
  }
@@ -249,23 +294,25 @@ function addRequiredArrays(overlay, schema) {
249
294
  // Generate extension overlays
250
295
  Object.entries(extensionMappings).forEach(([code, config]) => {
251
296
  console.log(`\nGenerating extension overlay: ${code} (${config.name})`);
252
-
297
+
253
298
  // Extract the specific paths
254
299
  let overlay = extractPathsWithNts2Ref(combinedSchema, config.paths);
255
-
300
+
256
301
  // Add required arrays where needed
257
302
  overlay = addRequiredArrays(overlay, combinedSchema);
258
-
303
+
259
304
  // Add schema metadata
260
305
  overlay.$schema = "http://json-schema.org/draft-07/schema#";
261
306
  overlay.$id = `https://trust.propdata.org.uk/schemas/v3/overlays/extensions/${code}.json`;
262
307
  overlay.$comment = config.description;
263
-
308
+
264
309
  // Write the overlay file
265
- const fileName = path.join(__dirname, `../schemas/v3/overlays/extensions/${code}.json`);
310
+ const fileName = path.join(
311
+ __dirname,
312
+ `../schemas/v3/overlays/extensions/${code}.json`
313
+ );
266
314
  fs.writeFileSync(fileName, JSON.stringify(overlay, null, 2));
267
315
  console.log(`Extension overlay ${code} written to ${fileName}`);
268
316
  });
269
317
 
270
-
271
- console.log("\nExtension overlay extraction complete!");
318
+ console.log("\nExtension overlay extraction complete!");