@pdtf/schemas 3.6.0-36 → 3.6.0-38

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 (39) hide show
  1. package/README.md +52 -33
  2. package/docs/sef25-extensions-ui-spec.md +258 -49
  3. package/index.js +9 -0
  4. package/package.json +1 -1
  5. package/src/schemas/v3/combined.json +271 -27
  6. package/src/schemas/v3/compactSkeleton.txt +16 -2
  7. package/src/schemas/v3/overlays/extensions/as.json +4 -2
  8. package/src/schemas/v3/overlays/extensions/dk.json +32 -0
  9. package/src/schemas/v3/overlays/extensions/dr.json +4 -2
  10. package/src/schemas/v3/overlays/extensions/er.json +4 -2
  11. package/src/schemas/v3/overlays/extensions/fd.json +6 -3
  12. package/src/schemas/v3/overlays/extensions/hi.json +6 -3
  13. package/src/schemas/v3/overlays/extensions/hs.json +4 -2
  14. package/src/schemas/v3/overlays/extensions/ic.json +59 -0
  15. package/src/schemas/v3/overlays/extensions/jk.json +4 -2
  16. package/src/schemas/v3/overlays/extensions/la.json +4 -2
  17. package/src/schemas/v3/overlays/extensions/lc.json +64 -0
  18. package/src/schemas/v3/overlays/extensions/ma.json +6 -3
  19. package/src/schemas/v3/overlays/extensions/mc.json +4 -2
  20. package/src/schemas/v3/overlays/extensions/mi.json +55 -0
  21. package/src/schemas/v3/overlays/extensions/nd.json +43 -0
  22. package/src/schemas/v3/overlays/extensions/oa.json +4 -2
  23. package/src/schemas/v3/overlays/extensions/oc.json +4 -2
  24. package/src/schemas/v3/overlays/extensions/pc.json +2 -1
  25. package/src/schemas/v3/overlays/extensions/ph.json +11 -14
  26. package/src/schemas/v3/overlays/extensions/rw.json +68 -0
  27. package/src/schemas/v3/overlays/extensions/sb.json +4 -2
  28. package/src/schemas/v3/overlays/extensions/sc.json +34 -10
  29. package/src/schemas/v3/overlays/extensions/sd.json +63 -0
  30. package/src/schemas/v3/overlays/extensions/sf.json +4 -2
  31. package/src/schemas/v3/overlays/extensions/sl.json +4 -2
  32. package/src/schemas/v3/overlays/extensions/tf.json +6 -3
  33. package/src/schemas/v3/overlays/extensions/tr.json +73 -0
  34. package/src/schemas/v3/overlays/extensions/wg.json +250 -0
  35. package/src/schemas/v3/overlays/sef25.json +717 -10
  36. package/src/schemas/v3/pdtf-transaction.json +232 -10
  37. package/src/schemas/v3/skeleton.json +24 -5
  38. package/src/tests/v3/extensionOverlays.test.js +411 -3
  39. package/src/utils/extractExtensionOverlays.js +116 -4
@@ -165,6 +165,15 @@ describe("Extension Overlays", () => {
165
165
  "sc",
166
166
  "pc",
167
167
  "ph", // SEF25
168
+ "dk",
169
+ "rw",
170
+ "sd",
171
+ "lc",
172
+ "wg",
173
+ "ic",
174
+ "nd",
175
+ "mi",
176
+ "tr", // SEF25 (new)
168
177
  ];
169
178
 
170
179
  expect(Object.keys(extensionOverlays).sort()).toEqual(
@@ -486,9 +495,14 @@ describe("Extension Overlays", () => {
486
495
  const cost = noBranch?.properties?.associatedCost;
487
496
  expect(cost).toBeDefined();
488
497
  expect(cost.ntsRef).toBe("U1.1");
489
- expect(cost.required).toEqual(["amount", "frequency"]);
490
- expect(cost.properties.amount.type).toBe("number");
498
+ expect(cost.required).toEqual(["frequency"]);
491
499
  expect(cost.properties.frequency.type).toBe("string");
500
+ // amount now lives inside the frequency-discriminator's "Per month"/"Per year" branch
501
+ const costBranch = cost.oneOf?.find((b) =>
502
+ b.properties?.frequency?.enum?.includes("Per month")
503
+ );
504
+ expect(costBranch?.properties?.amount?.type).toBe("number");
505
+ expect(costBranch?.required).toEqual(["amount"]);
492
506
  });
493
507
 
494
508
  test("should merge supply costs extension into mainsFoulDrainage No/Not known branch", () => {
@@ -504,7 +518,13 @@ describe("Extension Overlays", () => {
504
518
  const cost = noBranch?.properties?.associatedCost;
505
519
  expect(cost).toBeDefined();
506
520
  expect(cost.ntsRef).toBe("U1.2");
507
- expect(cost.required).toEqual(["amount", "frequency"]);
521
+ expect(cost.required).toEqual(["frequency"]);
522
+ // amount now lives inside the frequency-discriminator's "Per month"/"Per year" branch
523
+ const costBranch = cost.oneOf?.find((b) =>
524
+ b.properties?.frequency?.enum?.includes("Per month")
525
+ );
526
+ expect(costBranch?.properties?.amount?.type).toBe("number");
527
+ expect(costBranch?.required).toEqual(["amount"]);
508
528
  });
509
529
 
510
530
  test("should merge parking permit cost and frequency into controlledParking Yes branch", () => {
@@ -642,6 +662,25 @@ describe("Extension Overlays", () => {
642
662
  expect(result).toBe(true);
643
663
  });
644
664
 
665
+ test("should validate private water with no associated costs", () => {
666
+ const validator = getSubschemaValidator(
667
+ "/propertyPack/waterAndDrainage/water/mainsWater",
668
+ schemaId,
669
+ overlays
670
+ );
671
+
672
+ const result = validator({
673
+ yesNo: "No",
674
+ details: "Private borehole",
675
+ associatedCost: {
676
+ amount: 0,
677
+ frequency: "Not applicable",
678
+ },
679
+ });
680
+
681
+ expect(result).toBe(true);
682
+ });
683
+
645
684
  test("should require associated cost for private water supply", () => {
646
685
  const validator = getSubschemaValidator(
647
686
  "/propertyPack/waterAndDrainage/water/mainsWater",
@@ -957,5 +996,374 @@ describe("Extension Overlays", () => {
957
996
  );
958
997
  });
959
998
  });
999
+
1000
+ describe("Dropped kerb (dk)", () => {
1001
+ test("should validate Yes answer", () => {
1002
+ const validator = getSubschemaValidator(
1003
+ "/propertyPack/parking/droppedKerbAccess",
1004
+ schemaId,
1005
+ ["dk"]
1006
+ );
1007
+ expect(validator({ yesNo: "Yes" })).toBe(true);
1008
+ });
1009
+
1010
+ test("should validate No answer", () => {
1011
+ const validator = getSubschemaValidator(
1012
+ "/propertyPack/parking/droppedKerbAccess",
1013
+ schemaId,
1014
+ ["dk"]
1015
+ );
1016
+ expect(validator({ yesNo: "No" })).toBe(true);
1017
+ });
1018
+
1019
+ test("should reject invalid value", () => {
1020
+ const validator = getSubschemaValidator(
1021
+ "/propertyPack/parking/droppedKerbAccess",
1022
+ schemaId,
1023
+ ["dk"]
1024
+ );
1025
+ expect(validator({ yesNo: "Maybe" })).toBe(false);
1026
+ });
1027
+
1028
+ test("should not be required without dk overlay", () => {
1029
+ const schema = getTransactionSchema(schemaId, ["baspiV5"]);
1030
+ const parking = schema.properties?.propertyPack?.properties?.parking;
1031
+ expect(parking?.required || []).not.toContain("droppedKerbAccess");
1032
+ });
1033
+
1034
+ test("should be required with dk overlay", () => {
1035
+ const schema = getTransactionSchema(schemaId, ["dk"]);
1036
+ const parking = schema.properties?.propertyPack?.properties?.parking;
1037
+ expect(parking?.required).toContain("droppedKerbAccess");
1038
+ });
1039
+ });
1040
+
1041
+ describe("Private right of way (rw)", () => {
1042
+ test("should validate No answer", () => {
1043
+ const validator = getSubschemaValidator(
1044
+ "/propertyPack/rightsAndInformalArrangements/rightsOrArrangements/privateRightOfWay",
1045
+ schemaId,
1046
+ ["rw"]
1047
+ );
1048
+ expect(validator({ yesNo: "No" })).toBe(true);
1049
+ });
1050
+
1051
+ test("should validate Yes with details", () => {
1052
+ const validator = getSubschemaValidator(
1053
+ "/propertyPack/rightsAndInformalArrangements/rightsOrArrangements/privateRightOfWay",
1054
+ schemaId,
1055
+ ["rw"]
1056
+ );
1057
+ expect(validator({ yesNo: "Yes", details: "Neighbour has access through garden" })).toBe(true);
1058
+ });
1059
+
1060
+ test("should reject Yes without details", () => {
1061
+ const validator = getSubschemaValidator(
1062
+ "/propertyPack/rightsAndInformalArrangements/rightsOrArrangements/privateRightOfWay",
1063
+ schemaId,
1064
+ ["rw"]
1065
+ );
1066
+ expect(validator({ yesNo: "Yes" })).toBe(false);
1067
+ });
1068
+
1069
+ test("should reject Yes with empty details", () => {
1070
+ const validator = getSubschemaValidator(
1071
+ "/propertyPack/rightsAndInformalArrangements/rightsOrArrangements/privateRightOfWay",
1072
+ schemaId,
1073
+ ["rw"]
1074
+ );
1075
+ expect(validator({ yesNo: "Yes", details: "" })).toBe(false);
1076
+ });
1077
+
1078
+ test("should not be required without rw overlay", () => {
1079
+ const schema = getTransactionSchema(schemaId, ["baspiV5"]);
1080
+ const rights = schema.properties?.propertyPack?.properties?.rightsAndInformalArrangements
1081
+ ?.properties?.rightsOrArrangements;
1082
+ expect(rights?.required || []).not.toContain("privateRightOfWay");
1083
+ });
1084
+
1085
+ test("should be required with rw overlay", () => {
1086
+ const schema = getTransactionSchema(schemaId, ["rw"]);
1087
+ const rights = schema.properties?.propertyPack?.properties?.rightsAndInformalArrangements
1088
+ ?.properties?.rightsOrArrangements;
1089
+ expect(rights?.required).toContain("privateRightOfWay");
1090
+ });
1091
+ });
1092
+
1093
+ describe("Storm/fire/flood damage (sd)", () => {
1094
+ test("should validate No answer", () => {
1095
+ const validator = getSubschemaValidator(
1096
+ "/propertyPack/environmentalIssues/stormFireFloodDamage",
1097
+ schemaId,
1098
+ ["sd"]
1099
+ );
1100
+ expect(validator({ yesNo: "No" })).toBe(true);
1101
+ });
1102
+
1103
+ test("should validate Yes with details", () => {
1104
+ const validator = getSubschemaValidator(
1105
+ "/propertyPack/environmentalIssues/stormFireFloodDamage",
1106
+ schemaId,
1107
+ ["sd"]
1108
+ );
1109
+ expect(validator({ yesNo: "Yes", details: "Flood damage in 2020" })).toBe(true);
1110
+ });
1111
+
1112
+ test("should reject Yes without details", () => {
1113
+ const validator = getSubschemaValidator(
1114
+ "/propertyPack/environmentalIssues/stormFireFloodDamage",
1115
+ schemaId,
1116
+ ["sd"]
1117
+ );
1118
+ expect(validator({ yesNo: "Yes" })).toBe(false);
1119
+ });
1120
+
1121
+ test("should not be required without sd overlay", () => {
1122
+ const schema = getTransactionSchema(schemaId, ["baspiV5"]);
1123
+ const env = schema.properties?.propertyPack?.properties?.environmentalIssues;
1124
+ expect(env?.required || []).not.toContain("stormFireFloodDamage");
1125
+ });
1126
+
1127
+ test("should be required with sd overlay", () => {
1128
+ const schema = getTransactionSchema(schemaId, ["sd"]);
1129
+ const env = schema.properties?.propertyPack?.properties?.environmentalIssues;
1130
+ expect(env?.required).toContain("stormFireFloodDamage");
1131
+ });
1132
+ });
1133
+
1134
+ describe("Solar lease costs (lc)", () => {
1135
+ test("should validate Not applicable frequency", () => {
1136
+ const validator = getSubschemaValidator(
1137
+ "/propertyPack/electricity/solarPanels/panelsOwnedOutright/associatedCost",
1138
+ schemaId,
1139
+ ["lc"]
1140
+ );
1141
+ expect(validator({ amount: 0, frequency: "Not applicable" })).toBe(true);
1142
+ });
1143
+
1144
+ test("should validate amount and frequency", () => {
1145
+ const validator = getSubschemaValidator(
1146
+ "/propertyPack/electricity/solarPanels/panelsOwnedOutright/associatedCost",
1147
+ schemaId,
1148
+ ["lc"]
1149
+ );
1150
+ expect(validator({ amount: 75, frequency: "Per month" })).toBe(true);
1151
+ });
1152
+
1153
+ test("should reject missing amount", () => {
1154
+ const validator = getSubschemaValidator(
1155
+ "/propertyPack/electricity/solarPanels/panelsOwnedOutright/associatedCost",
1156
+ schemaId,
1157
+ ["lc"]
1158
+ );
1159
+ expect(validator({ frequency: "Per month" })).toBe(false);
1160
+ });
1161
+
1162
+ test("should reject missing frequency", () => {
1163
+ const validator = getSubschemaValidator(
1164
+ "/propertyPack/electricity/solarPanels/panelsOwnedOutright/associatedCost",
1165
+ schemaId,
1166
+ ["lc"]
1167
+ );
1168
+ expect(validator({ amount: 75 })).toBe(false);
1169
+ });
1170
+
1171
+ test("should reject invalid frequency", () => {
1172
+ const validator = getSubschemaValidator(
1173
+ "/propertyPack/electricity/solarPanels/panelsOwnedOutright/associatedCost",
1174
+ schemaId,
1175
+ ["lc"]
1176
+ );
1177
+ expect(validator({ amount: 75, frequency: "Weekly" })).toBe(false);
1178
+ });
1179
+ });
1180
+
1181
+ describe("Warranties and guarantees (wg)", () => {
1182
+ const warrantyCategoryPaths = [
1183
+ { path: "newHomeWarranty", ref: "W1.1" },
1184
+ { path: "roofingWork", ref: "W1.2" },
1185
+ { path: "dampProofingTreatment", ref: "W1.3" },
1186
+ { path: "centralHeatingAndorPlumbing", ref: "W1.4" },
1187
+ { path: "electricalRepairOrInstallation", ref: "W1.5" },
1188
+ { path: "subsidenceWork", ref: "W1.6" },
1189
+ { path: "otherGuarantees", ref: "W1.7" },
1190
+ ];
1191
+
1192
+ test("should have ntsRef on all warranty categories when wg overlay loaded", () => {
1193
+ const schema = getTransactionSchema(schemaId, ["baspiV5", "wg"]);
1194
+ const warranties =
1195
+ schema.properties?.propertyPack?.properties?.guaranteesWarrantiesAndIndemnityInsurances;
1196
+
1197
+ // Warranty fields are in oneOf[1] (hasValidGuaranteesOrWarranties = Yes)
1198
+ const yesBranch = warranties.oneOf[1];
1199
+
1200
+ warrantyCategoryPaths.forEach(({ path, ref }) => {
1201
+ expect(yesBranch.properties[path]?.ntsRef).toBe(ref);
1202
+ });
1203
+ });
1204
+
1205
+ test("should mark warranty categories as required when wg overlay loaded", () => {
1206
+ const schema = getTransactionSchema(schemaId, ["baspiV5", "wg"]);
1207
+ const warranties =
1208
+ schema.properties?.propertyPack?.properties?.guaranteesWarrantiesAndIndemnityInsurances;
1209
+
1210
+ const yesBranch = warranties.oneOf[1];
1211
+ warrantyCategoryPaths.forEach(({ path }) => {
1212
+ expect(yesBranch.required).toContain(path);
1213
+ });
1214
+ });
1215
+
1216
+ test("should not require warranty categories without any overlay", () => {
1217
+ const schema = getTransactionSchema(schemaId, []);
1218
+ const warranties =
1219
+ schema.properties?.propertyPack?.properties?.guaranteesWarrantiesAndIndemnityInsurances;
1220
+
1221
+ const yesBranch = warranties.oneOf[1];
1222
+ // Without any overlay, warranty categories should not be in required
1223
+ expect(yesBranch.required || []).not.toContain("newHomeWarranty");
1224
+ expect(yesBranch.required || []).not.toContain("electricalRepairOrInstallation");
1225
+ });
1226
+ });
1227
+
1228
+ describe("Insurance claims (ic)", () => {
1229
+ test("should mark insuranceClaims as required in isInsured=Yes branch", () => {
1230
+ const schema = getTransactionSchema(schemaId, ["ic"]);
1231
+ const insurance = schema.properties?.propertyPack?.properties?.insurance;
1232
+ // oneOf[1] is isInsured=Yes
1233
+ const yesBranch = insurance.oneOf[1];
1234
+ expect(yesBranch?.required).toContain("insuranceClaims");
1235
+ });
1236
+
1237
+ test("should validate insuranceClaims No answer", () => {
1238
+ const validator = getSubschemaValidator(
1239
+ "/propertyPack/insurance/insuranceClaims",
1240
+ schemaId,
1241
+ ["baspiV5", "ic"]
1242
+ );
1243
+ expect(validator({ yesNo: "No" })).toBe(true);
1244
+ });
1245
+
1246
+ test("should validate insuranceClaims Yes with details", () => {
1247
+ const validator = getSubschemaValidator(
1248
+ "/propertyPack/insurance/insuranceClaims",
1249
+ schemaId,
1250
+ ["baspiV5", "ic"]
1251
+ );
1252
+ expect(validator({ yesNo: "Yes", details: "Storm damage claim in 2019" })).toBe(true);
1253
+ });
1254
+ });
1255
+
1256
+ describe("Neighbour development (nd)", () => {
1257
+ test("should add ntsRef to neighbourDevelopment", () => {
1258
+ const schema = getTransactionSchema(schemaId, ["nd"]);
1259
+ const nd = schema.properties?.propertyPack?.properties?.notices
1260
+ ?.properties?.neighbourDevelopment;
1261
+ expect(nd?.ntsRef).toBe("N1.1");
1262
+ });
1263
+
1264
+ test("should add ntsRef via nd overlay", () => {
1265
+ const schema = getTransactionSchema(schemaId, ["nd"]);
1266
+ const notices = schema.properties?.propertyPack?.properties?.notices;
1267
+ expect(notices?.ntsRef).toBe("N1");
1268
+ });
1269
+
1270
+ test("should validate No answer", () => {
1271
+ const validator = getSubschemaValidator(
1272
+ "/propertyPack/notices/neighbourDevelopment",
1273
+ schemaId,
1274
+ ["baspiV5", "nd"]
1275
+ );
1276
+ expect(validator({ yesNo: "No" })).toBe(true);
1277
+ });
1278
+ });
1279
+
1280
+ describe("Other material issue (mi)", () => {
1281
+ test("should mark otherMaterialIssue as required", () => {
1282
+ const schema = getTransactionSchema(schemaId, ["mi"]);
1283
+ const additional = schema.properties?.propertyPack?.properties?.additionalInformation;
1284
+ expect(additional?.required).toContain("otherMaterialIssue");
1285
+ });
1286
+
1287
+ test("should not require otherMaterialIssue without mi overlay", () => {
1288
+ const schema = getTransactionSchema(schemaId, []);
1289
+ const additional = schema.properties?.propertyPack?.properties?.additionalInformation;
1290
+ expect(additional?.required || []).not.toContain("otherMaterialIssue");
1291
+ });
1292
+
1293
+ test("should validate No answer", () => {
1294
+ const validator = getSubschemaValidator(
1295
+ "/propertyPack/additionalInformation/otherMaterialIssue",
1296
+ schemaId,
1297
+ ["baspiV5", "mi"]
1298
+ );
1299
+ expect(validator({ yesNo: "No" })).toBe(true);
1300
+ });
1301
+
1302
+ test("should validate Yes with details", () => {
1303
+ const validator = getSubschemaValidator(
1304
+ "/propertyPack/additionalInformation/otherMaterialIssue",
1305
+ schemaId,
1306
+ ["baspiV5", "mi"]
1307
+ );
1308
+ expect(validator({ yesNo: "Yes", details: "Previous subsidence issues" })).toBe(true);
1309
+ });
1310
+ });
1311
+
1312
+ describe("Title restrictions (tr)", () => {
1313
+ test("should require titleRestrictions in freehold branch with tr overlay", () => {
1314
+ const schema = getTransactionSchema(schemaId, ["tr"]);
1315
+ const items = schema.properties?.propertyPack?.properties?.ownership
1316
+ ?.properties?.ownershipsToBeTransferred?.items;
1317
+ // oneOf[0] is Freehold
1318
+ const freeholdBranch = items.oneOf[0];
1319
+ expect(freeholdBranch?.properties?.titleRestrictions).toBeDefined();
1320
+ expect(freeholdBranch?.required).toContain("titleRestrictions");
1321
+ });
1322
+
1323
+ test("should not require titleRestrictions without tr overlay", () => {
1324
+ const schema = getTransactionSchema(schemaId, ["baspiV5"]);
1325
+ const items = schema.properties?.propertyPack?.properties?.ownership
1326
+ ?.properties?.ownershipsToBeTransferred?.items;
1327
+ const freeholdBranch = items.oneOf[0];
1328
+ expect(freeholdBranch?.required || []).not.toContain("titleRestrictions");
1329
+ });
1330
+
1331
+ test("should have correct ntsRef", () => {
1332
+ const schema = getTransactionSchema(schemaId, ["tr"]);
1333
+ const items = schema.properties?.propertyPack?.properties?.ownership
1334
+ ?.properties?.ownershipsToBeTransferred?.items;
1335
+ const freeholdBranch = items.oneOf[0];
1336
+ expect(freeholdBranch?.properties?.titleRestrictions?.ntsRef).toBe("T1.1");
1337
+ });
1338
+ });
1339
+
1340
+ describe("All SEF25 extensions combined", () => {
1341
+ const allSef25 = ["sc", "pc", "ph", "dk", "rw", "sd", "lc", "wg", "ic", "nd", "mi", "tr"];
1342
+
1343
+ test("should load all SEF25 extensions together without error", () => {
1344
+ const schema = getTransactionSchema(schemaId, ["baspiV5", ...allSef25]);
1345
+ expect(schema).toBeDefined();
1346
+ expect(schema.properties?.propertyPack).toBeDefined();
1347
+ });
1348
+
1349
+ test("should not interfere with existing NTS2 extensions when combined", () => {
1350
+ const schema = getTransactionSchema(schemaId, ["nts2025", ...allSef25]);
1351
+
1352
+ // NTS2 fields still work
1353
+ const si = schema.properties?.propertyPack?.properties?.specialistIssues;
1354
+ expect(si?.properties?.wellsDitchesShaft?.ntsRef).toBe("H1.1");
1355
+
1356
+ // New SEF25 fields present
1357
+ const parking = schema.properties?.propertyPack?.properties?.parking?.properties;
1358
+ expect(parking?.droppedKerbAccess).toBeDefined();
1359
+
1360
+ const env = schema.properties?.propertyPack?.properties?.environmentalIssues?.properties;
1361
+ expect(env?.stormFireFloodDamage).toBeDefined();
1362
+
1363
+ const rights = schema.properties?.propertyPack?.properties?.rightsAndInformalArrangements
1364
+ ?.properties?.rightsOrArrangements?.properties;
1365
+ expect(rights?.privateRightOfWay).toBeDefined();
1366
+ });
1367
+ });
960
1368
  });
961
1369
  });
@@ -214,10 +214,97 @@ const extensionMappings = {
214
214
  description: "Property hazards and known issues",
215
215
  refType: "sef25Ref",
216
216
  paths: [
217
- "/properties/propertyPack/properties/specialistIssues/properties/wellsDitchesShaft",
218
- "/properties/propertyPack/properties/specialistIssues/properties/damagedOrExposedElectrics",
219
- "/properties/propertyPack/properties/specialistIssues/properties/damageToFlooringOrStaircases",
220
- "/properties/propertyPack/properties/specialistIssues/properties/knownAreasInPoorCondition",
217
+ "/properties/propertyPack/properties/specialistIssues",
218
+ ],
219
+ },
220
+
221
+ // Dropped kerb access to parking
222
+ dk: {
223
+ name: "droppedKerbAccess",
224
+ description: "Dropped kerb access to parking",
225
+ refType: "sef25Ref",
226
+ paths: [
227
+ "/properties/propertyPack/properties/parking/properties/droppedKerbAccess",
228
+ ],
229
+ },
230
+
231
+ // Private right of way
232
+ rw: {
233
+ name: "privateRightOfWay",
234
+ description: "Private right of way",
235
+ refType: "sef25Ref",
236
+ paths: [
237
+ "/properties/propertyPack/properties/rightsAndInformalArrangements/properties/rightsOrArrangements/properties/privateRightOfWay",
238
+ ],
239
+ },
240
+
241
+ // Storm, fire or flood damage
242
+ sd: {
243
+ name: "stormFireFloodDamage",
244
+ description: "Storm, fire or flood damage",
245
+ refType: "sef25Ref",
246
+ paths: [
247
+ "/properties/propertyPack/properties/environmentalIssues/properties/stormFireFloodDamage",
248
+ ],
249
+ },
250
+
251
+ // Solar panel lease costs
252
+ lc: {
253
+ name: "solarLeaseCosts",
254
+ description: "Solar panel lease costs",
255
+ refType: "sef25Ref",
256
+ paths: [
257
+ "/properties/propertyPack/properties/electricity/properties/solarPanels/oneOf/1/properties/panelsOwnedOutright/oneOf/1/properties/associatedCost",
258
+ ],
259
+ },
260
+
261
+ // Warranties and guarantees upfront
262
+ wg: {
263
+ name: "warrantiesGuarantees",
264
+ description: "Warranties and guarantees upfront Y/N for SEF25",
265
+ refType: "sef25Ref",
266
+ paths: [
267
+ "/properties/propertyPack/properties/guaranteesWarrantiesAndIndemnityInsurances",
268
+ ],
269
+ },
270
+
271
+ // Insurance claims upfront
272
+ ic: {
273
+ name: "insuranceClaims",
274
+ description: "Insurance claims upfront for SEF25",
275
+ refType: "sef25Ref",
276
+ paths: [
277
+ "/properties/propertyPack/properties/insurance/oneOf/1/properties/insuranceClaims",
278
+ ],
279
+ },
280
+
281
+ // Neighbour development upfront
282
+ nd: {
283
+ name: "neighbourDevelopment",
284
+ description: "Neighbour development upfront for SEF25",
285
+ refType: "sef25Ref",
286
+ paths: [
287
+ "/properties/propertyPack/properties/notices/properties/neighbourDevelopment",
288
+ ],
289
+ },
290
+
291
+ // Other material issue upfront
292
+ mi: {
293
+ name: "otherMaterialIssue",
294
+ description: "Other material issue upfront for SEF25",
295
+ refType: "sef25Ref",
296
+ paths: [
297
+ "/properties/propertyPack/properties/additionalInformation/properties/otherMaterialIssue",
298
+ ],
299
+ },
300
+
301
+ // Title restrictions for freehold
302
+ tr: {
303
+ name: "titleRestrictions",
304
+ description: "Title restrictions for freehold properties",
305
+ refType: "sef25Ref",
306
+ paths: [
307
+ "/properties/propertyPack/properties/ownership/properties/ownershipsToBeTransferred/items/oneOf/0/properties/titleRestrictions",
221
308
  ],
222
309
  },
223
310
  };
@@ -405,6 +492,28 @@ function addRequiredArrays(overlay, schema, refConfig) {
405
492
  return overlay;
406
493
  }
407
494
 
495
+ // Annotate intermediate nodes with refs from the source schema
496
+ // This ensures every level of the overlay tree carries an ntsRef
497
+ function addIntermediateRefs(overlay, schema, refConfig) {
498
+ traverse(overlay).forEach(function (node) {
499
+ if (node && typeof node === "object" && !Array.isArray(node) && node.properties) {
500
+ // Skip if this node already has a ref
501
+ if (node[refConfig.outputRef]) return;
502
+
503
+ const schemaPath = "/" + this.path.join("/");
504
+ try {
505
+ const schemaNode = jp.get(schema, schemaPath);
506
+ if (schemaNode && schemaNode[refConfig.ref]) {
507
+ node[refConfig.outputRef] = schemaNode[refConfig.ref];
508
+ }
509
+ } catch (err) {
510
+ // Path doesn't exist in schema, skip
511
+ }
512
+ }
513
+ });
514
+ return overlay;
515
+ }
516
+
408
517
  // Generate extension overlays
409
518
  Object.entries(extensionMappings).forEach(([code, config]) => {
410
519
  const refType = config.refType || "nts2Ref";
@@ -418,6 +527,9 @@ Object.entries(extensionMappings).forEach(([code, config]) => {
418
527
  // Add required arrays where needed
419
528
  overlay = addRequiredArrays(overlay, combinedSchema, refConfig);
420
529
 
530
+ // Add refs to intermediate nodes in the path hierarchy
531
+ overlay = addIntermediateRefs(overlay, combinedSchema, refConfig);
532
+
421
533
  // Add schema metadata
422
534
  overlay.$schema = "http://json-schema.org/draft-07/schema#";
423
535
  overlay.$id = `https://trust.propdata.org.uk/schemas/v3/overlays/extensions/${code}.json`;