@pdtf/schemas 3.6.0-31 → 3.6.0-33

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 +288 -32
  6. package/src/schemas/v3/compactSkeleton.txt +17 -0
  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 +86 -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/rw.json +68 -0
  26. package/src/schemas/v3/overlays/extensions/sb.json +4 -2
  27. package/src/schemas/v3/overlays/extensions/sc.json +82 -14
  28. package/src/schemas/v3/overlays/extensions/sd.json +63 -0
  29. package/src/schemas/v3/overlays/extensions/sf.json +4 -2
  30. package/src/schemas/v3/overlays/extensions/sl.json +4 -2
  31. package/src/schemas/v3/overlays/extensions/tf.json +6 -3
  32. package/src/schemas/v3/overlays/extensions/tr.json +73 -0
  33. package/src/schemas/v3/overlays/extensions/wg.json +209 -0
  34. package/src/schemas/v3/overlays/sef25.json +966 -0
  35. package/src/schemas/v3/pdtf-transaction.json +256 -94
  36. package/src/schemas/v3/skeleton.json +23 -1
  37. package/src/tests/v3/extensionOverlays.test.js +417 -11
  38. package/src/utils/extractExtensionOverlays.js +121 -0
  39. package/src/utils/extractOverlay.js +1 -0
@@ -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(
@@ -485,10 +494,15 @@ describe("Extension Overlays", () => {
485
494
  const noBranch = mainsWater.oneOf[0];
486
495
  const cost = noBranch?.properties?.associatedCost;
487
496
  expect(cost).toBeDefined();
488
- expect(cost.sef25Ref).toBe("U1.1");
489
- expect(cost.required).toEqual(["amount", "frequency"]);
490
- expect(cost.properties.amount.type).toBe("number");
491
- expect(cost.properties.frequency.type).toBe("string");
497
+ expect(cost.ntsRef).toBe("U1.1");
498
+ expect(cost.required).toEqual(["hasCost"]);
499
+ expect(cost.properties.hasCost.type).toBe("string");
500
+ expect(cost.oneOf).toHaveLength(2);
501
+ // Yes branch should have amount and frequency
502
+ const yesBranch = cost.oneOf[1];
503
+ expect(yesBranch.properties.amount.type).toBe("number");
504
+ expect(yesBranch.properties.frequency.type).toBe("string");
505
+ expect(yesBranch.required).toEqual(["amount", "frequency"]);
492
506
  });
493
507
 
494
508
  test("should merge supply costs extension into mainsFoulDrainage No/Not known branch", () => {
@@ -503,8 +517,8 @@ describe("Extension Overlays", () => {
503
517
  const noBranch = mainsFoul.oneOf[2];
504
518
  const cost = noBranch?.properties?.associatedCost;
505
519
  expect(cost).toBeDefined();
506
- expect(cost.sef25Ref).toBe("U1.2");
507
- expect(cost.required).toEqual(["amount", "frequency"]);
520
+ expect(cost.ntsRef).toBe("U1.2");
521
+ expect(cost.required).toEqual(["hasCost"]);
508
522
  });
509
523
 
510
524
  test("should merge parking permit cost and frequency into controlledParking Yes branch", () => {
@@ -519,12 +533,12 @@ describe("Extension Overlays", () => {
519
533
  const yesBranch = controlled.oneOf[1];
520
534
  const cost = yesBranch?.properties?.costOfPermit;
521
535
  expect(cost).toBeDefined();
522
- expect(cost.sef25Ref).toBe("P1.1");
536
+ expect(cost.ntsRef).toBe("P1.1");
523
537
  expect(cost.type).toBe("number");
524
538
 
525
539
  const freq = yesBranch?.properties?.costOfPermitFrequency;
526
540
  expect(freq).toBeDefined();
527
- expect(freq.sef25Ref).toBe("P1.2");
541
+ expect(freq.ntsRef).toBe("P1.2");
528
542
  expect(freq.type).toBe("string");
529
543
  });
530
544
 
@@ -543,7 +557,7 @@ describe("Extension Overlays", () => {
543
557
 
544
558
  expectedFields.forEach(({ key, ref }) => {
545
559
  expect(si[key]).toBeDefined();
546
- expect(si[key].sef25Ref).toBe(ref);
560
+ expect(si[key].ntsRef).toBe(ref);
547
561
  expect(si[key].required).toContain("yesNo");
548
562
  expect(si[key].discriminator?.propertyName).toBe("yesNo");
549
563
  expect(si[key].oneOf).toHaveLength(2);
@@ -634,6 +648,7 @@ describe("Extension Overlays", () => {
634
648
  yesNo: "No",
635
649
  details: "Private borehole",
636
650
  associatedCost: {
651
+ hasCost: "Yes",
637
652
  amount: 50,
638
653
  frequency: "Per month",
639
654
  },
@@ -642,6 +657,24 @@ describe("Extension Overlays", () => {
642
657
  expect(result).toBe(true);
643
658
  });
644
659
 
660
+ test("should validate private water with no associated costs", () => {
661
+ const validator = getSubschemaValidator(
662
+ "/propertyPack/waterAndDrainage/water/mainsWater",
663
+ schemaId,
664
+ overlays
665
+ );
666
+
667
+ const result = validator({
668
+ yesNo: "No",
669
+ details: "Private borehole",
670
+ associatedCost: {
671
+ hasCost: "No",
672
+ },
673
+ });
674
+
675
+ expect(result).toBe(true);
676
+ });
677
+
645
678
  test("should require associated cost for private water supply", () => {
646
679
  const validator = getSubschemaValidator(
647
680
  "/propertyPack/waterAndDrainage/water/mainsWater",
@@ -669,6 +702,7 @@ describe("Extension Overlays", () => {
669
702
  yesNo: "No",
670
703
  details: "Borehole",
671
704
  associatedCost: {
705
+ hasCost: "Yes",
672
706
  amount: 100,
673
707
  frequency: "Weekly",
674
708
  },
@@ -688,6 +722,7 @@ describe("Extension Overlays", () => {
688
722
  yesNo: "No",
689
723
  details: "Borehole",
690
724
  associatedCost: {
725
+ hasCost: "Yes",
691
726
  amount: "fifty pounds",
692
727
  frequency: "Per month",
693
728
  },
@@ -707,6 +742,7 @@ describe("Extension Overlays", () => {
707
742
  const result = validator({
708
743
  yesNo: "No",
709
744
  associatedCost: {
745
+ hasCost: "Yes",
710
746
  amount: 200,
711
747
  frequency: "Per year",
712
748
  },
@@ -725,6 +761,7 @@ describe("Extension Overlays", () => {
725
761
  const result = validator({
726
762
  yesNo: "No",
727
763
  associatedCost: {
764
+ hasCost: "Yes",
728
765
  amount: 200,
729
766
  frequency: "Quarterly",
730
767
  },
@@ -951,11 +988,380 @@ describe("Extension Overlays", () => {
951
988
  expect(si?.required).toContain("ongoingHealthOrSafetyIssue");
952
989
 
953
990
  // SEF25 hazards also present
954
- expect(si?.properties?.wellsDitchesShaft?.sef25Ref).toBe("H1.1");
955
- expect(si?.properties?.knownAreasInPoorCondition?.sef25Ref).toBe(
991
+ expect(si?.properties?.wellsDitchesShaft?.ntsRef).toBe("H1.1");
992
+ expect(si?.properties?.knownAreasInPoorCondition?.ntsRef).toBe(
956
993
  "H1.4"
957
994
  );
958
995
  });
959
996
  });
997
+
998
+ describe("Dropped kerb (dk)", () => {
999
+ test("should validate Yes answer", () => {
1000
+ const validator = getSubschemaValidator(
1001
+ "/propertyPack/parking/droppedKerbAccess",
1002
+ schemaId,
1003
+ ["dk"]
1004
+ );
1005
+ expect(validator({ yesNo: "Yes" })).toBe(true);
1006
+ });
1007
+
1008
+ test("should validate No answer", () => {
1009
+ const validator = getSubschemaValidator(
1010
+ "/propertyPack/parking/droppedKerbAccess",
1011
+ schemaId,
1012
+ ["dk"]
1013
+ );
1014
+ expect(validator({ yesNo: "No" })).toBe(true);
1015
+ });
1016
+
1017
+ test("should reject invalid value", () => {
1018
+ const validator = getSubschemaValidator(
1019
+ "/propertyPack/parking/droppedKerbAccess",
1020
+ schemaId,
1021
+ ["dk"]
1022
+ );
1023
+ expect(validator({ yesNo: "Maybe" })).toBe(false);
1024
+ });
1025
+
1026
+ test("should not be required without dk overlay", () => {
1027
+ const schema = getTransactionSchema(schemaId, ["baspiV5"]);
1028
+ const parking = schema.properties?.propertyPack?.properties?.parking;
1029
+ expect(parking?.required || []).not.toContain("droppedKerbAccess");
1030
+ });
1031
+
1032
+ test("should be required with dk overlay", () => {
1033
+ const schema = getTransactionSchema(schemaId, ["dk"]);
1034
+ const parking = schema.properties?.propertyPack?.properties?.parking;
1035
+ expect(parking?.required).toContain("droppedKerbAccess");
1036
+ });
1037
+ });
1038
+
1039
+ describe("Private right of way (rw)", () => {
1040
+ test("should validate No answer", () => {
1041
+ const validator = getSubschemaValidator(
1042
+ "/propertyPack/rightsAndInformalArrangements/rightsOrArrangements/privateRightOfWay",
1043
+ schemaId,
1044
+ ["rw"]
1045
+ );
1046
+ expect(validator({ yesNo: "No" })).toBe(true);
1047
+ });
1048
+
1049
+ test("should validate Yes with details", () => {
1050
+ const validator = getSubschemaValidator(
1051
+ "/propertyPack/rightsAndInformalArrangements/rightsOrArrangements/privateRightOfWay",
1052
+ schemaId,
1053
+ ["rw"]
1054
+ );
1055
+ expect(validator({ yesNo: "Yes", details: "Neighbour has access through garden" })).toBe(true);
1056
+ });
1057
+
1058
+ test("should reject Yes without details", () => {
1059
+ const validator = getSubschemaValidator(
1060
+ "/propertyPack/rightsAndInformalArrangements/rightsOrArrangements/privateRightOfWay",
1061
+ schemaId,
1062
+ ["rw"]
1063
+ );
1064
+ expect(validator({ yesNo: "Yes" })).toBe(false);
1065
+ });
1066
+
1067
+ test("should reject Yes with empty details", () => {
1068
+ const validator = getSubschemaValidator(
1069
+ "/propertyPack/rightsAndInformalArrangements/rightsOrArrangements/privateRightOfWay",
1070
+ schemaId,
1071
+ ["rw"]
1072
+ );
1073
+ expect(validator({ yesNo: "Yes", details: "" })).toBe(false);
1074
+ });
1075
+
1076
+ test("should not be required without rw overlay", () => {
1077
+ const schema = getTransactionSchema(schemaId, ["baspiV5"]);
1078
+ const rights = schema.properties?.propertyPack?.properties?.rightsAndInformalArrangements
1079
+ ?.properties?.rightsOrArrangements;
1080
+ expect(rights?.required || []).not.toContain("privateRightOfWay");
1081
+ });
1082
+
1083
+ test("should be required with rw overlay", () => {
1084
+ const schema = getTransactionSchema(schemaId, ["rw"]);
1085
+ const rights = schema.properties?.propertyPack?.properties?.rightsAndInformalArrangements
1086
+ ?.properties?.rightsOrArrangements;
1087
+ expect(rights?.required).toContain("privateRightOfWay");
1088
+ });
1089
+ });
1090
+
1091
+ describe("Storm/fire/flood damage (sd)", () => {
1092
+ test("should validate No answer", () => {
1093
+ const validator = getSubschemaValidator(
1094
+ "/propertyPack/environmentalIssues/stormFireFloodDamage",
1095
+ schemaId,
1096
+ ["sd"]
1097
+ );
1098
+ expect(validator({ yesNo: "No" })).toBe(true);
1099
+ });
1100
+
1101
+ test("should validate Yes with details", () => {
1102
+ const validator = getSubschemaValidator(
1103
+ "/propertyPack/environmentalIssues/stormFireFloodDamage",
1104
+ schemaId,
1105
+ ["sd"]
1106
+ );
1107
+ expect(validator({ yesNo: "Yes", details: "Flood damage in 2020" })).toBe(true);
1108
+ });
1109
+
1110
+ test("should reject Yes without details", () => {
1111
+ const validator = getSubschemaValidator(
1112
+ "/propertyPack/environmentalIssues/stormFireFloodDamage",
1113
+ schemaId,
1114
+ ["sd"]
1115
+ );
1116
+ expect(validator({ yesNo: "Yes" })).toBe(false);
1117
+ });
1118
+
1119
+ test("should not be required without sd overlay", () => {
1120
+ const schema = getTransactionSchema(schemaId, ["baspiV5"]);
1121
+ const env = schema.properties?.propertyPack?.properties?.environmentalIssues;
1122
+ expect(env?.required || []).not.toContain("stormFireFloodDamage");
1123
+ });
1124
+
1125
+ test("should be required with sd overlay", () => {
1126
+ const schema = getTransactionSchema(schemaId, ["sd"]);
1127
+ const env = schema.properties?.propertyPack?.properties?.environmentalIssues;
1128
+ expect(env?.required).toContain("stormFireFloodDamage");
1129
+ });
1130
+ });
1131
+
1132
+ describe("Solar lease costs (lc)", () => {
1133
+ test("should validate No costs", () => {
1134
+ const validator = getSubschemaValidator(
1135
+ "/propertyPack/electricity/solarPanels/panelsOwnedOutright/associatedCost",
1136
+ schemaId,
1137
+ ["lc"]
1138
+ );
1139
+ expect(validator({ hasCost: "No" })).toBe(true);
1140
+ });
1141
+
1142
+ test("should validate Yes with amount and frequency", () => {
1143
+ const validator = getSubschemaValidator(
1144
+ "/propertyPack/electricity/solarPanels/panelsOwnedOutright/associatedCost",
1145
+ schemaId,
1146
+ ["lc"]
1147
+ );
1148
+ expect(validator({ hasCost: "Yes", amount: 75, frequency: "Per month" })).toBe(true);
1149
+ });
1150
+
1151
+ test("should reject Yes without amount", () => {
1152
+ const validator = getSubschemaValidator(
1153
+ "/propertyPack/electricity/solarPanels/panelsOwnedOutright/associatedCost",
1154
+ schemaId,
1155
+ ["lc"]
1156
+ );
1157
+ expect(validator({ hasCost: "Yes", frequency: "Per month" })).toBe(false);
1158
+ });
1159
+
1160
+ test("should reject Yes without frequency", () => {
1161
+ const validator = getSubschemaValidator(
1162
+ "/propertyPack/electricity/solarPanels/panelsOwnedOutright/associatedCost",
1163
+ schemaId,
1164
+ ["lc"]
1165
+ );
1166
+ expect(validator({ hasCost: "Yes", amount: 75 })).toBe(false);
1167
+ });
1168
+
1169
+ test("should reject invalid frequency", () => {
1170
+ const validator = getSubschemaValidator(
1171
+ "/propertyPack/electricity/solarPanels/panelsOwnedOutright/associatedCost",
1172
+ schemaId,
1173
+ ["lc"]
1174
+ );
1175
+ expect(validator({ hasCost: "Yes", amount: 75, frequency: "Weekly" })).toBe(false);
1176
+ });
1177
+ });
1178
+
1179
+ describe("Warranties and guarantees (wg)", () => {
1180
+ const warrantyCategoryPaths = [
1181
+ { path: "newHomeWarranty", ref: "W1.1" },
1182
+ { path: "roofingWork", ref: "W1.2" },
1183
+ { path: "dampProofingTreatment", ref: "W1.3" },
1184
+ { path: "centralHeatingAndorPlumbing", ref: "W1.4" },
1185
+ { path: "electricalRepairOrInstallation", ref: "W1.5" },
1186
+ { path: "subsidenceWork", ref: "W1.6" },
1187
+ { path: "otherGuarantees", ref: "W1.7" },
1188
+ ];
1189
+
1190
+ test("should have ntsRef on all warranty categories when wg overlay loaded", () => {
1191
+ const schema = getTransactionSchema(schemaId, ["baspiV5", "wg"]);
1192
+ const warranties =
1193
+ schema.properties?.propertyPack?.properties?.guaranteesWarrantiesAndIndemnityInsurances;
1194
+
1195
+ // Warranty fields are in oneOf[1] (hasValidGuaranteesOrWarranties = Yes)
1196
+ const yesBranch = warranties.oneOf[1];
1197
+
1198
+ warrantyCategoryPaths.forEach(({ path, ref }) => {
1199
+ expect(yesBranch.properties[path]?.ntsRef).toBe(ref);
1200
+ });
1201
+ });
1202
+
1203
+ test("should mark warranty categories as required when wg overlay loaded", () => {
1204
+ const schema = getTransactionSchema(schemaId, ["baspiV5", "wg"]);
1205
+ const warranties =
1206
+ schema.properties?.propertyPack?.properties?.guaranteesWarrantiesAndIndemnityInsurances;
1207
+
1208
+ const yesBranch = warranties.oneOf[1];
1209
+ warrantyCategoryPaths.forEach(({ path }) => {
1210
+ expect(yesBranch.required).toContain(path);
1211
+ });
1212
+ });
1213
+
1214
+ test("should not require warranty categories without any overlay", () => {
1215
+ const schema = getTransactionSchema(schemaId, []);
1216
+ const warranties =
1217
+ schema.properties?.propertyPack?.properties?.guaranteesWarrantiesAndIndemnityInsurances;
1218
+
1219
+ const yesBranch = warranties.oneOf[1];
1220
+ // Without any overlay, warranty categories should not be in required
1221
+ expect(yesBranch.required || []).not.toContain("newHomeWarranty");
1222
+ expect(yesBranch.required || []).not.toContain("electricalRepairOrInstallation");
1223
+ });
1224
+ });
1225
+
1226
+ describe("Insurance claims (ic)", () => {
1227
+ test("should mark insuranceClaims as required in isInsured=Yes branch", () => {
1228
+ const schema = getTransactionSchema(schemaId, ["ic"]);
1229
+ const insurance = schema.properties?.propertyPack?.properties?.insurance;
1230
+ // oneOf[1] is isInsured=Yes
1231
+ const yesBranch = insurance.oneOf[1];
1232
+ expect(yesBranch?.required).toContain("insuranceClaims");
1233
+ });
1234
+
1235
+ test("should validate insuranceClaims No answer", () => {
1236
+ const validator = getSubschemaValidator(
1237
+ "/propertyPack/insurance/insuranceClaims",
1238
+ schemaId,
1239
+ ["baspiV5", "ic"]
1240
+ );
1241
+ expect(validator({ yesNo: "No" })).toBe(true);
1242
+ });
1243
+
1244
+ test("should validate insuranceClaims Yes with details", () => {
1245
+ const validator = getSubschemaValidator(
1246
+ "/propertyPack/insurance/insuranceClaims",
1247
+ schemaId,
1248
+ ["baspiV5", "ic"]
1249
+ );
1250
+ expect(validator({ yesNo: "Yes", details: "Storm damage claim in 2019" })).toBe(true);
1251
+ });
1252
+ });
1253
+
1254
+ describe("Neighbour development (nd)", () => {
1255
+ test("should add ntsRef to neighbourDevelopment", () => {
1256
+ const schema = getTransactionSchema(schemaId, ["nd"]);
1257
+ const nd = schema.properties?.propertyPack?.properties?.notices
1258
+ ?.properties?.neighbourDevelopment;
1259
+ expect(nd?.ntsRef).toBe("N1.1");
1260
+ });
1261
+
1262
+ test("should add ntsRef via nd overlay", () => {
1263
+ const schema = getTransactionSchema(schemaId, ["nd"]);
1264
+ const notices = schema.properties?.propertyPack?.properties?.notices;
1265
+ expect(notices?.ntsRef).toBe("N1");
1266
+ });
1267
+
1268
+ test("should validate No answer", () => {
1269
+ const validator = getSubschemaValidator(
1270
+ "/propertyPack/notices/neighbourDevelopment",
1271
+ schemaId,
1272
+ ["baspiV5", "nd"]
1273
+ );
1274
+ expect(validator({ yesNo: "No" })).toBe(true);
1275
+ });
1276
+ });
1277
+
1278
+ describe("Other material issue (mi)", () => {
1279
+ test("should mark otherMaterialIssue as required", () => {
1280
+ const schema = getTransactionSchema(schemaId, ["mi"]);
1281
+ const additional = schema.properties?.propertyPack?.properties?.additionalInformation;
1282
+ expect(additional?.required).toContain("otherMaterialIssue");
1283
+ });
1284
+
1285
+ test("should not require otherMaterialIssue without mi overlay", () => {
1286
+ const schema = getTransactionSchema(schemaId, []);
1287
+ const additional = schema.properties?.propertyPack?.properties?.additionalInformation;
1288
+ expect(additional?.required || []).not.toContain("otherMaterialIssue");
1289
+ });
1290
+
1291
+ test("should validate No answer", () => {
1292
+ const validator = getSubschemaValidator(
1293
+ "/propertyPack/additionalInformation/otherMaterialIssue",
1294
+ schemaId,
1295
+ ["baspiV5", "mi"]
1296
+ );
1297
+ expect(validator({ yesNo: "No" })).toBe(true);
1298
+ });
1299
+
1300
+ test("should validate Yes with details", () => {
1301
+ const validator = getSubschemaValidator(
1302
+ "/propertyPack/additionalInformation/otherMaterialIssue",
1303
+ schemaId,
1304
+ ["baspiV5", "mi"]
1305
+ );
1306
+ expect(validator({ yesNo: "Yes", details: "Previous subsidence issues" })).toBe(true);
1307
+ });
1308
+ });
1309
+
1310
+ describe("Title restrictions (tr)", () => {
1311
+ test("should require titleRestrictions in freehold branch with tr overlay", () => {
1312
+ const schema = getTransactionSchema(schemaId, ["tr"]);
1313
+ const items = schema.properties?.propertyPack?.properties?.ownership
1314
+ ?.properties?.ownershipsToBeTransferred?.items;
1315
+ // oneOf[0] is Freehold
1316
+ const freeholdBranch = items.oneOf[0];
1317
+ expect(freeholdBranch?.properties?.titleRestrictions).toBeDefined();
1318
+ expect(freeholdBranch?.required).toContain("titleRestrictions");
1319
+ });
1320
+
1321
+ test("should not require titleRestrictions without tr overlay", () => {
1322
+ const schema = getTransactionSchema(schemaId, ["baspiV5"]);
1323
+ const items = schema.properties?.propertyPack?.properties?.ownership
1324
+ ?.properties?.ownershipsToBeTransferred?.items;
1325
+ const freeholdBranch = items.oneOf[0];
1326
+ expect(freeholdBranch?.required || []).not.toContain("titleRestrictions");
1327
+ });
1328
+
1329
+ test("should have correct ntsRef", () => {
1330
+ const schema = getTransactionSchema(schemaId, ["tr"]);
1331
+ const items = schema.properties?.propertyPack?.properties?.ownership
1332
+ ?.properties?.ownershipsToBeTransferred?.items;
1333
+ const freeholdBranch = items.oneOf[0];
1334
+ expect(freeholdBranch?.properties?.titleRestrictions?.ntsRef).toBe("T1.1");
1335
+ });
1336
+ });
1337
+
1338
+ describe("All SEF25 extensions combined", () => {
1339
+ const allSef25 = ["sc", "pc", "ph", "dk", "rw", "sd", "lc", "wg", "ic", "nd", "mi", "tr"];
1340
+
1341
+ test("should load all SEF25 extensions together without error", () => {
1342
+ const schema = getTransactionSchema(schemaId, ["baspiV5", ...allSef25]);
1343
+ expect(schema).toBeDefined();
1344
+ expect(schema.properties?.propertyPack).toBeDefined();
1345
+ });
1346
+
1347
+ test("should not interfere with existing NTS2 extensions when combined", () => {
1348
+ const schema = getTransactionSchema(schemaId, ["nts2025", ...allSef25]);
1349
+
1350
+ // NTS2 fields still work
1351
+ const si = schema.properties?.propertyPack?.properties?.specialistIssues;
1352
+ expect(si?.properties?.wellsDitchesShaft?.ntsRef).toBe("H1.1");
1353
+
1354
+ // New SEF25 fields present
1355
+ const parking = schema.properties?.propertyPack?.properties?.parking?.properties;
1356
+ expect(parking?.droppedKerbAccess).toBeDefined();
1357
+
1358
+ const env = schema.properties?.propertyPack?.properties?.environmentalIssues?.properties;
1359
+ expect(env?.stormFireFloodDamage).toBeDefined();
1360
+
1361
+ const rights = schema.properties?.propertyPack?.properties?.rightsAndInformalArrangements
1362
+ ?.properties?.rightsOrArrangements?.properties;
1363
+ expect(rights?.privateRightOfWay).toBeDefined();
1364
+ });
1365
+ });
960
1366
  });
961
1367
  });
@@ -220,6 +220,102 @@ const extensionMappings = {
220
220
  "/properties/propertyPack/properties/specialistIssues/properties/knownAreasInPoorCondition",
221
221
  ],
222
222
  },
223
+
224
+ // Dropped kerb access to parking
225
+ dk: {
226
+ name: "droppedKerbAccess",
227
+ description: "Dropped kerb access to parking",
228
+ refType: "sef25Ref",
229
+ paths: [
230
+ "/properties/propertyPack/properties/parking/properties/droppedKerbAccess",
231
+ ],
232
+ },
233
+
234
+ // Private right of way
235
+ rw: {
236
+ name: "privateRightOfWay",
237
+ description: "Private right of way",
238
+ refType: "sef25Ref",
239
+ paths: [
240
+ "/properties/propertyPack/properties/rightsAndInformalArrangements/properties/rightsOrArrangements/properties/privateRightOfWay",
241
+ ],
242
+ },
243
+
244
+ // Storm, fire or flood damage
245
+ sd: {
246
+ name: "stormFireFloodDamage",
247
+ description: "Storm, fire or flood damage",
248
+ refType: "sef25Ref",
249
+ paths: [
250
+ "/properties/propertyPack/properties/environmentalIssues/properties/stormFireFloodDamage",
251
+ ],
252
+ },
253
+
254
+ // Solar panel lease costs
255
+ lc: {
256
+ name: "solarLeaseCosts",
257
+ description: "Solar panel lease costs",
258
+ refType: "sef25Ref",
259
+ paths: [
260
+ "/properties/propertyPack/properties/electricity/properties/solarPanels/oneOf/1/properties/panelsOwnedOutright/oneOf/1/properties/associatedCost",
261
+ ],
262
+ },
263
+
264
+ // Warranties and guarantees upfront
265
+ wg: {
266
+ name: "warrantiesGuarantees",
267
+ description: "Warranties and guarantees upfront Y/N for SEF25",
268
+ refType: "sef25Ref",
269
+ paths: [
270
+ "/properties/propertyPack/properties/guaranteesWarrantiesAndIndemnityInsurances/oneOf/1/properties/newHomeWarranty",
271
+ "/properties/propertyPack/properties/guaranteesWarrantiesAndIndemnityInsurances/oneOf/1/properties/roofingWork",
272
+ "/properties/propertyPack/properties/guaranteesWarrantiesAndIndemnityInsurances/oneOf/1/properties/dampProofingTreatment",
273
+ "/properties/propertyPack/properties/guaranteesWarrantiesAndIndemnityInsurances/oneOf/1/properties/centralHeatingAndorPlumbing",
274
+ "/properties/propertyPack/properties/guaranteesWarrantiesAndIndemnityInsurances/oneOf/1/properties/electricalRepairOrInstallation",
275
+ "/properties/propertyPack/properties/guaranteesWarrantiesAndIndemnityInsurances/oneOf/1/properties/subsidenceWork",
276
+ "/properties/propertyPack/properties/guaranteesWarrantiesAndIndemnityInsurances/oneOf/1/properties/otherGuarantees",
277
+ ],
278
+ },
279
+
280
+ // Insurance claims upfront
281
+ ic: {
282
+ name: "insuranceClaims",
283
+ description: "Insurance claims upfront for SEF25",
284
+ refType: "sef25Ref",
285
+ paths: [
286
+ "/properties/propertyPack/properties/insurance/oneOf/1/properties/insuranceClaims",
287
+ ],
288
+ },
289
+
290
+ // Neighbour development upfront
291
+ nd: {
292
+ name: "neighbourDevelopment",
293
+ description: "Neighbour development upfront for SEF25",
294
+ refType: "sef25Ref",
295
+ paths: [
296
+ "/properties/propertyPack/properties/notices/properties/neighbourDevelopment",
297
+ ],
298
+ },
299
+
300
+ // Other material issue upfront
301
+ mi: {
302
+ name: "otherMaterialIssue",
303
+ description: "Other material issue upfront for SEF25",
304
+ refType: "sef25Ref",
305
+ paths: [
306
+ "/properties/propertyPack/properties/additionalInformation/properties/otherMaterialIssue",
307
+ ],
308
+ },
309
+
310
+ // Title restrictions for freehold
311
+ tr: {
312
+ name: "titleRestrictions",
313
+ description: "Title restrictions for freehold properties",
314
+ refType: "sef25Ref",
315
+ paths: [
316
+ "/properties/propertyPack/properties/ownership/properties/ownershipsToBeTransferred/items/oneOf/0/properties/titleRestrictions",
317
+ ],
318
+ },
223
319
  };
224
320
 
225
321
  // Function to extract properties at specific paths with given ref type
@@ -405,6 +501,28 @@ function addRequiredArrays(overlay, schema, refConfig) {
405
501
  return overlay;
406
502
  }
407
503
 
504
+ // Annotate intermediate nodes with refs from the source schema
505
+ // This ensures every level of the overlay tree carries an ntsRef
506
+ function addIntermediateRefs(overlay, schema, refConfig) {
507
+ traverse(overlay).forEach(function (node) {
508
+ if (node && typeof node === "object" && !Array.isArray(node) && node.properties) {
509
+ // Skip if this node already has a ref
510
+ if (node[refConfig.outputRef]) return;
511
+
512
+ const schemaPath = "/" + this.path.join("/");
513
+ try {
514
+ const schemaNode = jp.get(schema, schemaPath);
515
+ if (schemaNode && schemaNode[refConfig.ref]) {
516
+ node[refConfig.outputRef] = schemaNode[refConfig.ref];
517
+ }
518
+ } catch (err) {
519
+ // Path doesn't exist in schema, skip
520
+ }
521
+ }
522
+ });
523
+ return overlay;
524
+ }
525
+
408
526
  // Generate extension overlays
409
527
  Object.entries(extensionMappings).forEach(([code, config]) => {
410
528
  const refType = config.refType || "nts2Ref";
@@ -418,6 +536,9 @@ Object.entries(extensionMappings).forEach(([code, config]) => {
418
536
  // Add required arrays where needed
419
537
  overlay = addRequiredArrays(overlay, combinedSchema, refConfig);
420
538
 
539
+ // Add refs to intermediate nodes in the path hierarchy
540
+ overlay = addIntermediateRefs(overlay, combinedSchema, refConfig);
541
+
421
542
  // Add schema metadata
422
543
  overlay.$schema = "http://json-schema.org/draft-07/schema#";
423
544
  overlay.$id = `https://trust.propdata.org.uk/schemas/v3/overlays/extensions/${code}.json`;
@@ -42,6 +42,7 @@ const extractFields = [
42
42
  "rds",
43
43
  "oc1",
44
44
  "sr24",
45
+ "sef25",
45
46
  ];
46
47
 
47
48
  const flattenSkeleton = (schema) => {