@pdtf/schemas 3.6.0-dev.3 → 3.6.0-dev.4

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 (263) hide show
  1. package/charming-napier-57a77c/LICENSE +21 -0
  2. package/charming-napier-57a77c/README.md +430 -0
  3. package/charming-napier-57a77c/docs/sef25-extensions-ui-spec.md +355 -0
  4. package/charming-napier-57a77c/index.js +654 -0
  5. package/charming-napier-57a77c/package-lock.json +7297 -0
  6. package/charming-napier-57a77c/package.json +41 -0
  7. package/charming-napier-57a77c/src/examples/v1/exampleAddParticipantVouch.json +32 -0
  8. package/charming-napier-57a77c/src/examples/v1/exampleDocumentedVouch.json +40 -0
  9. package/charming-napier-57a77c/src/examples/v1/exampleElectronicRecord.json +30 -0
  10. package/charming-napier-57a77c/src/examples/v1/exampleTransaction.json +878 -0
  11. package/charming-napier-57a77c/src/examples/v1/exampleVouch.json +25 -0
  12. package/charming-napier-57a77c/src/examples/v2/exampleTransaction.json +901 -0
  13. package/charming-napier-57a77c/src/examples/v3/exampleAddParticipantVouch.json +38 -0
  14. package/charming-napier-57a77c/src/examples/v3/exampleCustomOverlay.json +55 -0
  15. package/charming-napier-57a77c/src/examples/v3/exampleCustomOverlay2.json +93 -0
  16. package/charming-napier-57a77c/src/examples/v3/exampleDocumentedVouch.json +47 -0
  17. package/charming-napier-57a77c/src/examples/v3/exampleElectronicRecord.json +36 -0
  18. package/charming-napier-57a77c/src/examples/v3/exampleTransaction.json +1038 -0
  19. package/charming-napier-57a77c/src/examples/v3/exampleVouch.json +32 -0
  20. package/charming-napier-57a77c/src/mappings/SendPropertyDetailsRequestSchema.json +1588 -0
  21. package/charming-napier-57a77c/src/mappings/adf.js +198 -0
  22. package/charming-napier-57a77c/src/schemas/v1/GeoJSON.json +1279 -0
  23. package/charming-napier-57a77c/src/schemas/v1/baspiNotes.html +102 -0
  24. package/charming-napier-57a77c/src/schemas/v1/energy-performance-certificate.json +124 -0
  25. package/charming-napier-57a77c/src/schemas/v1/freehold-information.json +2051 -0
  26. package/charming-napier-57a77c/src/schemas/v1/leasehold-information.json +2064 -0
  27. package/charming-napier-57a77c/src/schemas/v1/legal-information.json +1045 -0
  28. package/charming-napier-57a77c/src/schemas/v1/material-facts.json +2109 -0
  29. package/charming-napier-57a77c/src/schemas/v1/pdtf-transaction-search-results.json +40 -0
  30. package/charming-napier-57a77c/src/schemas/v1/pdtf-transaction.json +191 -0
  31. package/charming-napier-57a77c/src/schemas/v1/searches/drainage-and-water.json +261 -0
  32. package/charming-napier-57a77c/src/schemas/v1/searches/local-land-charges.json +69 -0
  33. package/charming-napier-57a77c/src/schemas/v1/searches/local-searches-required.json +857 -0
  34. package/charming-napier-57a77c/src/schemas/v1/searches.json +237 -0
  35. package/charming-napier-57a77c/src/schemas/v1/title-deed.json +841 -0
  36. package/charming-napier-57a77c/src/schemas/v2/baspiNotes.html +102 -0
  37. package/charming-napier-57a77c/src/schemas/v2/combined.json +37731 -0
  38. package/charming-napier-57a77c/src/schemas/v2/overlays/baspi.json +8132 -0
  39. package/charming-napier-57a77c/src/schemas/v2/overlays/con29DW.json +705 -0
  40. package/charming-napier-57a77c/src/schemas/v2/overlays/con29R.json +2956 -0
  41. package/charming-napier-57a77c/src/schemas/v2/overlays/fme1.json +1259 -0
  42. package/charming-napier-57a77c/src/schemas/v2/overlays/llc1.json +25 -0
  43. package/charming-napier-57a77c/src/schemas/v2/overlays/lpe1.json +2171 -0
  44. package/charming-napier-57a77c/src/schemas/v2/overlays/nts.json +326 -0
  45. package/charming-napier-57a77c/src/schemas/v2/overlays/oc1.json +2799 -0
  46. package/charming-napier-57a77c/src/schemas/v2/overlays/piq.json +3419 -0
  47. package/charming-napier-57a77c/src/schemas/v2/overlays/rds.json +3736 -0
  48. package/charming-napier-57a77c/src/schemas/v2/overlays/ta10.json +5306 -0
  49. package/charming-napier-57a77c/src/schemas/v2/overlays/ta6.json +4533 -0
  50. package/charming-napier-57a77c/src/schemas/v2/overlays/ta7.json +1045 -0
  51. package/charming-napier-57a77c/src/schemas/v2/pdtf-transaction.json +34351 -0
  52. package/charming-napier-57a77c/src/schemas/v2/skeleton.json +3973 -0
  53. package/charming-napier-57a77c/src/schemas/v3/combined.json +48153 -0
  54. package/charming-napier-57a77c/src/schemas/v3/compactSkeleton.txt +4457 -0
  55. package/charming-napier-57a77c/src/schemas/v3/overlays/README.md +299 -0
  56. package/charming-napier-57a77c/src/schemas/v3/overlays/baspi4.json +8027 -0
  57. package/charming-napier-57a77c/src/schemas/v3/overlays/baspi5.json +8177 -0
  58. package/charming-napier-57a77c/src/schemas/v3/overlays/con29DW.json +716 -0
  59. package/charming-napier-57a77c/src/schemas/v3/overlays/con29R.json +2981 -0
  60. package/charming-napier-57a77c/src/schemas/v3/overlays/extensions/ac.json +333 -0
  61. package/charming-napier-57a77c/src/schemas/v3/overlays/extensions/as.json +74 -0
  62. package/charming-napier-57a77c/src/schemas/v3/overlays/extensions/dk.json +32 -0
  63. package/charming-napier-57a77c/src/schemas/v3/overlays/extensions/dr.json +73 -0
  64. package/charming-napier-57a77c/src/schemas/v3/overlays/extensions/er.json +85 -0
  65. package/charming-napier-57a77c/src/schemas/v3/overlays/extensions/fd.json +75 -0
  66. package/charming-napier-57a77c/src/schemas/v3/overlays/extensions/hi.json +51 -0
  67. package/charming-napier-57a77c/src/schemas/v3/overlays/extensions/hs.json +73 -0
  68. package/charming-napier-57a77c/src/schemas/v3/overlays/extensions/ic.json +59 -0
  69. package/charming-napier-57a77c/src/schemas/v3/overlays/extensions/jk.json +96 -0
  70. package/charming-napier-57a77c/src/schemas/v3/overlays/extensions/la.json +85 -0
  71. package/charming-napier-57a77c/src/schemas/v3/overlays/extensions/lc.json +75 -0
  72. package/charming-napier-57a77c/src/schemas/v3/overlays/extensions/ma.json +90 -0
  73. package/charming-napier-57a77c/src/schemas/v3/overlays/extensions/mc.json +38 -0
  74. package/charming-napier-57a77c/src/schemas/v3/overlays/extensions/mi.json +69 -0
  75. package/charming-napier-57a77c/src/schemas/v3/overlays/extensions/nd.json +43 -0
  76. package/charming-napier-57a77c/src/schemas/v3/overlays/extensions/oa.json +24 -0
  77. package/charming-napier-57a77c/src/schemas/v3/overlays/extensions/oc.json +57 -0
  78. package/charming-napier-57a77c/src/schemas/v3/overlays/extensions/pc.json +37 -0
  79. package/charming-napier-57a77c/src/schemas/v3/overlays/extensions/ph.json +194 -0
  80. package/charming-napier-57a77c/src/schemas/v3/overlays/extensions/rw.json +68 -0
  81. package/charming-napier-57a77c/src/schemas/v3/overlays/extensions/sb.json +73 -0
  82. package/charming-napier-57a77c/src/schemas/v3/overlays/extensions/sc.json +129 -0
  83. package/charming-napier-57a77c/src/schemas/v3/overlays/extensions/sd.json +63 -0
  84. package/charming-napier-57a77c/src/schemas/v3/overlays/extensions/sf.json +74 -0
  85. package/charming-napier-57a77c/src/schemas/v3/overlays/extensions/sl.json +68 -0
  86. package/charming-napier-57a77c/src/schemas/v3/overlays/extensions/ta.json +240 -0
  87. package/charming-napier-57a77c/src/schemas/v3/overlays/extensions/tf.json +103 -0
  88. package/charming-napier-57a77c/src/schemas/v3/overlays/extensions/tr.json +73 -0
  89. package/charming-napier-57a77c/src/schemas/v3/overlays/extensions/wg.json +350 -0
  90. package/charming-napier-57a77c/src/schemas/v3/overlays/fme1.json +1266 -0
  91. package/charming-napier-57a77c/src/schemas/v3/overlays/llc1.json +25 -0
  92. package/charming-napier-57a77c/src/schemas/v3/overlays/lpe1.json +2199 -0
  93. package/charming-napier-57a77c/src/schemas/v3/overlays/nts.json +2157 -0
  94. package/charming-napier-57a77c/src/schemas/v3/overlays/nts2.json +2742 -0
  95. package/charming-napier-57a77c/src/schemas/v3/overlays/ntsl.json +1813 -0
  96. package/charming-napier-57a77c/src/schemas/v3/overlays/ntsl2.json +2217 -0
  97. package/charming-napier-57a77c/src/schemas/v3/overlays/oc1.json +2817 -0
  98. package/charming-napier-57a77c/src/schemas/v3/overlays/piq.json +3321 -0
  99. package/charming-napier-57a77c/src/schemas/v3/overlays/rds.json +3665 -0
  100. package/charming-napier-57a77c/src/schemas/v3/overlays/sef25.json +1639 -0
  101. package/charming-napier-57a77c/src/schemas/v3/overlays/sr24.json +38 -0
  102. package/charming-napier-57a77c/src/schemas/v3/overlays/ta10.json +4654 -0
  103. package/charming-napier-57a77c/src/schemas/v3/overlays/ta6.json +4568 -0
  104. package/charming-napier-57a77c/src/schemas/v3/overlays/ta6ed6.json +6537 -0
  105. package/charming-napier-57a77c/src/schemas/v3/overlays/ta7.json +1537 -0
  106. package/charming-napier-57a77c/src/schemas/v3/overlays/ta7ed5.json +1551 -0
  107. package/charming-napier-57a77c/src/schemas/v3/pdtf-transaction.json +41229 -0
  108. package/charming-napier-57a77c/src/schemas/v3/skeleton.json +5703 -0
  109. package/charming-napier-57a77c/src/schemas/verifiedClaims/README.md +21 -0
  110. package/charming-napier-57a77c/src/schemas/verifiedClaims/pdtf-verified-claims.json +447 -0
  111. package/charming-napier-57a77c/src/tests/v3/buyerCircumstances.test.js +543 -0
  112. package/charming-napier-57a77c/src/tests/v3/caching.test.js +591 -0
  113. package/charming-napier-57a77c/src/tests/v3/enquiries.test.js +1096 -0
  114. package/charming-napier-57a77c/src/tests/v3/extensionOverlays.test.js +1599 -0
  115. package/charming-napier-57a77c/src/tests/v3/offers.test.js +823 -0
  116. package/charming-napier-57a77c/src/tests/v3/patternProperties.test.js +688 -0
  117. package/charming-napier-57a77c/src/tests/v3/ta6ed6.test.js +2374 -0
  118. package/charming-napier-57a77c/src/tests/v3/ta7ed5.test.js +600 -0
  119. package/charming-napier-57a77c/src/tests/v3/transactionSchema.test.js +646 -0
  120. package/charming-napier-57a77c/src/tests/v3/verifiedClaimsValidator.test.js +195 -0
  121. package/charming-napier-57a77c/src/utils/compactSkeleton.js +84 -0
  122. package/charming-napier-57a77c/src/utils/countTokens.js +67 -0
  123. package/charming-napier-57a77c/src/utils/extractExtensionOverlays.js +624 -0
  124. package/charming-napier-57a77c/src/utils/extractOverlay.js +381 -0
  125. package/charming-napier-57a77c/src/utils/extractProperties.js +44 -0
  126. package/charming-napier-57a77c/src/utils/listPathTitles.js +36 -0
  127. package/charming-napier-57a77c/src/utils/minimalSkeleton.js +85 -0
  128. package/charming-napier-57a77c/src/utils/pathList.csv +903 -0
  129. package/charming-napier-57a77c/src/utils/pathSkeleton.js +181 -0
  130. package/keen-dewdney-6ae15b/LICENSE +21 -0
  131. package/keen-dewdney-6ae15b/README.md +334 -0
  132. package/keen-dewdney-6ae15b/index.js +557 -0
  133. package/keen-dewdney-6ae15b/package-lock.json +7296 -0
  134. package/keen-dewdney-6ae15b/package.json +39 -0
  135. package/keen-dewdney-6ae15b/src/examples/v1/exampleAddParticipantVouch.json +32 -0
  136. package/keen-dewdney-6ae15b/src/examples/v1/exampleDocumentedVouch.json +40 -0
  137. package/keen-dewdney-6ae15b/src/examples/v1/exampleElectronicRecord.json +30 -0
  138. package/keen-dewdney-6ae15b/src/examples/v1/exampleTransaction.json +878 -0
  139. package/keen-dewdney-6ae15b/src/examples/v1/exampleVouch.json +25 -0
  140. package/keen-dewdney-6ae15b/src/examples/v2/exampleTransaction.json +901 -0
  141. package/keen-dewdney-6ae15b/src/examples/v3/exampleAddParticipantVouch.json +35 -0
  142. package/keen-dewdney-6ae15b/src/examples/v3/exampleCustomOverlay.json +55 -0
  143. package/keen-dewdney-6ae15b/src/examples/v3/exampleCustomOverlay2.json +93 -0
  144. package/keen-dewdney-6ae15b/src/examples/v3/exampleDocumentedVouch.json +44 -0
  145. package/keen-dewdney-6ae15b/src/examples/v3/exampleElectronicRecord.json +33 -0
  146. package/keen-dewdney-6ae15b/src/examples/v3/exampleTransaction.json +1034 -0
  147. package/keen-dewdney-6ae15b/src/examples/v3/exampleVouch.json +29 -0
  148. package/keen-dewdney-6ae15b/src/mappings/SendPropertyDetailsRequestSchema.json +1588 -0
  149. package/keen-dewdney-6ae15b/src/mappings/adf.js +198 -0
  150. package/keen-dewdney-6ae15b/src/schemas/v1/GeoJSON.json +1279 -0
  151. package/keen-dewdney-6ae15b/src/schemas/v1/baspiNotes.html +102 -0
  152. package/keen-dewdney-6ae15b/src/schemas/v1/energy-performance-certificate.json +124 -0
  153. package/keen-dewdney-6ae15b/src/schemas/v1/freehold-information.json +2051 -0
  154. package/keen-dewdney-6ae15b/src/schemas/v1/leasehold-information.json +2064 -0
  155. package/keen-dewdney-6ae15b/src/schemas/v1/legal-information.json +1045 -0
  156. package/keen-dewdney-6ae15b/src/schemas/v1/material-facts.json +2109 -0
  157. package/keen-dewdney-6ae15b/src/schemas/v1/pdtf-transaction-search-results.json +40 -0
  158. package/keen-dewdney-6ae15b/src/schemas/v1/pdtf-transaction.json +191 -0
  159. package/keen-dewdney-6ae15b/src/schemas/v1/searches/drainage-and-water.json +261 -0
  160. package/keen-dewdney-6ae15b/src/schemas/v1/searches/local-land-charges.json +69 -0
  161. package/keen-dewdney-6ae15b/src/schemas/v1/searches/local-searches-required.json +857 -0
  162. package/keen-dewdney-6ae15b/src/schemas/v1/searches.json +237 -0
  163. package/keen-dewdney-6ae15b/src/schemas/v1/title-deed.json +841 -0
  164. package/keen-dewdney-6ae15b/src/schemas/v2/baspiNotes.html +102 -0
  165. package/keen-dewdney-6ae15b/src/schemas/v2/combined.json +37731 -0
  166. package/keen-dewdney-6ae15b/src/schemas/v2/overlays/baspi.json +8132 -0
  167. package/keen-dewdney-6ae15b/src/schemas/v2/overlays/con29DW.json +705 -0
  168. package/keen-dewdney-6ae15b/src/schemas/v2/overlays/con29R.json +2956 -0
  169. package/keen-dewdney-6ae15b/src/schemas/v2/overlays/fme1.json +1259 -0
  170. package/keen-dewdney-6ae15b/src/schemas/v2/overlays/llc1.json +25 -0
  171. package/keen-dewdney-6ae15b/src/schemas/v2/overlays/lpe1.json +2171 -0
  172. package/keen-dewdney-6ae15b/src/schemas/v2/overlays/nts.json +326 -0
  173. package/keen-dewdney-6ae15b/src/schemas/v2/overlays/oc1.json +2799 -0
  174. package/keen-dewdney-6ae15b/src/schemas/v2/overlays/piq.json +3419 -0
  175. package/keen-dewdney-6ae15b/src/schemas/v2/overlays/rds.json +3736 -0
  176. package/keen-dewdney-6ae15b/src/schemas/v2/overlays/ta10.json +5306 -0
  177. package/keen-dewdney-6ae15b/src/schemas/v2/overlays/ta6.json +4533 -0
  178. package/keen-dewdney-6ae15b/src/schemas/v2/overlays/ta7.json +1045 -0
  179. package/keen-dewdney-6ae15b/src/schemas/v2/pdtf-transaction.json +34351 -0
  180. package/keen-dewdney-6ae15b/src/schemas/v2/skeleton.json +3973 -0
  181. package/keen-dewdney-6ae15b/src/schemas/v3/combined.json +41720 -0
  182. package/keen-dewdney-6ae15b/src/schemas/v3/compactSkeleton.txt +3325 -0
  183. package/keen-dewdney-6ae15b/src/schemas/v3/overlays/README.md +299 -0
  184. package/keen-dewdney-6ae15b/src/schemas/v3/overlays/baspi4.json +7872 -0
  185. package/keen-dewdney-6ae15b/src/schemas/v3/overlays/baspi5.json +8019 -0
  186. package/keen-dewdney-6ae15b/src/schemas/v3/overlays/con29DW.json +716 -0
  187. package/keen-dewdney-6ae15b/src/schemas/v3/overlays/con29R.json +2981 -0
  188. package/keen-dewdney-6ae15b/src/schemas/v3/overlays/extensions/as.json +72 -0
  189. package/keen-dewdney-6ae15b/src/schemas/v3/overlays/extensions/dr.json +73 -0
  190. package/keen-dewdney-6ae15b/src/schemas/v3/overlays/extensions/er.json +83 -0
  191. package/keen-dewdney-6ae15b/src/schemas/v3/overlays/extensions/fd.json +72 -0
  192. package/keen-dewdney-6ae15b/src/schemas/v3/overlays/extensions/hi.json +48 -0
  193. package/keen-dewdney-6ae15b/src/schemas/v3/overlays/extensions/hs.json +71 -0
  194. package/keen-dewdney-6ae15b/src/schemas/v3/overlays/extensions/jk.json +76 -0
  195. package/keen-dewdney-6ae15b/src/schemas/v3/overlays/extensions/la.json +83 -0
  196. package/keen-dewdney-6ae15b/src/schemas/v3/overlays/extensions/ma.json +87 -0
  197. package/keen-dewdney-6ae15b/src/schemas/v3/overlays/extensions/mc.json +36 -0
  198. package/keen-dewdney-6ae15b/src/schemas/v3/overlays/extensions/oa.json +22 -0
  199. package/keen-dewdney-6ae15b/src/schemas/v3/overlays/extensions/oc.json +55 -0
  200. package/keen-dewdney-6ae15b/src/schemas/v3/overlays/extensions/sb.json +71 -0
  201. package/keen-dewdney-6ae15b/src/schemas/v3/overlays/extensions/sf.json +72 -0
  202. package/keen-dewdney-6ae15b/src/schemas/v3/overlays/extensions/sl.json +66 -0
  203. package/keen-dewdney-6ae15b/src/schemas/v3/overlays/extensions/tf.json +100 -0
  204. package/keen-dewdney-6ae15b/src/schemas/v3/overlays/fme1.json +1264 -0
  205. package/keen-dewdney-6ae15b/src/schemas/v3/overlays/llc1.json +25 -0
  206. package/keen-dewdney-6ae15b/src/schemas/v3/overlays/lpe1.json +2226 -0
  207. package/keen-dewdney-6ae15b/src/schemas/v3/overlays/nts.json +2090 -0
  208. package/keen-dewdney-6ae15b/src/schemas/v3/overlays/nts2.json +2678 -0
  209. package/keen-dewdney-6ae15b/src/schemas/v3/overlays/ntsl.json +1746 -0
  210. package/keen-dewdney-6ae15b/src/schemas/v3/overlays/ntsl2.json +2153 -0
  211. package/keen-dewdney-6ae15b/src/schemas/v3/overlays/oc1.json +2817 -0
  212. package/keen-dewdney-6ae15b/src/schemas/v3/overlays/piq.json +3276 -0
  213. package/keen-dewdney-6ae15b/src/schemas/v3/overlays/rds.json +3552 -0
  214. package/keen-dewdney-6ae15b/src/schemas/v3/overlays/sr24.json +38 -0
  215. package/keen-dewdney-6ae15b/src/schemas/v3/overlays/ta10.json +4642 -0
  216. package/keen-dewdney-6ae15b/src/schemas/v3/overlays/ta6.json +4501 -0
  217. package/keen-dewdney-6ae15b/src/schemas/v3/overlays/ta7.json +1435 -0
  218. package/keen-dewdney-6ae15b/src/schemas/v3/pdtf-transaction.json +37225 -0
  219. package/keen-dewdney-6ae15b/src/schemas/v3/skeleton.json +4323 -0
  220. package/keen-dewdney-6ae15b/src/schemas/verifiedClaims/README.md +12 -0
  221. package/keen-dewdney-6ae15b/src/schemas/verifiedClaims/pdtf-verified-claims.json +431 -0
  222. package/keen-dewdney-6ae15b/src/tests/v3/caching.test.js +591 -0
  223. package/keen-dewdney-6ae15b/src/tests/v3/extensionOverlays.test.js +525 -0
  224. package/keen-dewdney-6ae15b/src/tests/v3/transactionSchema.test.js +574 -0
  225. package/keen-dewdney-6ae15b/src/tests/v3/verifiedClaimsValidator.test.js +128 -0
  226. package/keen-dewdney-6ae15b/src/utils/compactSkeleton.js +84 -0
  227. package/keen-dewdney-6ae15b/src/utils/countTokens.js +67 -0
  228. package/keen-dewdney-6ae15b/src/utils/extractExtensionOverlays.js +362 -0
  229. package/keen-dewdney-6ae15b/src/utils/extractOverlay.js +290 -0
  230. package/keen-dewdney-6ae15b/src/utils/extractProperties.js +44 -0
  231. package/keen-dewdney-6ae15b/src/utils/listPathTitles.js +36 -0
  232. package/keen-dewdney-6ae15b/src/utils/minimalSkeleton.js +85 -0
  233. package/keen-dewdney-6ae15b/src/utils/pathList.csv +903 -0
  234. package/keen-dewdney-6ae15b/src/utils/pathSkeleton.js +181 -0
  235. package/package.json +1 -1
  236. package/src/schemas/v3/combined.json +8 -15
  237. package/src/schemas/v3/overlays/baspi4.json +12 -7
  238. package/src/schemas/v3/overlays/baspi5.json +12 -7
  239. package/src/schemas/v3/overlays/extensions/ac.json +2 -1
  240. package/src/schemas/v3/overlays/extensions/dk.json +3 -2
  241. package/src/schemas/v3/overlays/extensions/ic.json +2 -1
  242. package/src/schemas/v3/overlays/extensions/lc.json +3 -2
  243. package/src/schemas/v3/overlays/extensions/mi.json +2 -1
  244. package/src/schemas/v3/overlays/extensions/nd.json +3 -2
  245. package/src/schemas/v3/overlays/extensions/pc.json +3 -2
  246. package/src/schemas/v3/overlays/extensions/ph.json +2 -1
  247. package/src/schemas/v3/overlays/extensions/rw.json +4 -3
  248. package/src/schemas/v3/overlays/extensions/sc.json +8 -4
  249. package/src/schemas/v3/overlays/extensions/sd.json +3 -2
  250. package/src/schemas/v3/overlays/extensions/tr.json +3 -2
  251. package/src/schemas/v3/overlays/extensions/wg.json +2 -1
  252. package/src/schemas/v3/overlays/lpe1.json +1 -1
  253. package/src/schemas/v3/overlays/nts.json +3 -1
  254. package/src/schemas/v3/overlays/nts2.json +3 -1
  255. package/src/schemas/v3/overlays/ntsl.json +3 -1
  256. package/src/schemas/v3/overlays/ntsl2.json +3 -1
  257. package/src/schemas/v3/overlays/piq.json +7 -5
  258. package/src/schemas/v3/overlays/rds.json +9 -6
  259. package/src/schemas/v3/overlays/ta10.json +0 -9
  260. package/src/schemas/v3/overlays/ta6.json +8 -4
  261. package/src/schemas/v3/overlays/ta6ed6.json +4 -2
  262. package/src/schemas/v3/pdtf-transaction.json +15 -18
  263. package/src/utils/extractExtensionOverlays.js +10 -1
@@ -0,0 +1,1599 @@
1
+ const {
2
+ getTransactionSchema,
3
+ extensionOverlays,
4
+ getValidator,
5
+ getSubschemaValidator,
6
+ } = require("../../../index.js");
7
+
8
+ const schemaId =
9
+ "https://trust.propdata.org.uk/schemas/v3/pdtf-transaction.json";
10
+
11
+ describe("Extension Overlays", () => {
12
+ describe("String key support", () => {
13
+ test("should load extension overlays using string keys", () => {
14
+ const schema = getTransactionSchema(schemaId, ["nts2023", "jk"]);
15
+ const jkProp =
16
+ schema.properties?.propertyPack?.properties?.specialistIssues
17
+ ?.properties?.japaneseKnotweed;
18
+
19
+ expect(jkProp).toBeDefined();
20
+ expect(jkProp.ntsRef).toBe("A5.3");
21
+ });
22
+
23
+ test("should work with multiple extension string keys", () => {
24
+ const schema = getTransactionSchema(schemaId, [
25
+ "nts2023",
26
+ "jk",
27
+ "tf",
28
+ "ma",
29
+ "er",
30
+ ]);
31
+
32
+ const jkProp =
33
+ schema.properties?.propertyPack?.properties?.specialistIssues
34
+ ?.properties?.japaneseKnotweed;
35
+ const tfProp =
36
+ schema.properties?.propertyPack?.properties?.ownership?.properties
37
+ ?.ownershipsToBeTransferred?.items?.oneOf?.[2]?.properties
38
+ ?.leaseholdInformation?.properties?.serviceCharge?.oneOf?.[1]
39
+ ?.properties?.transferFees;
40
+ const maProp =
41
+ schema.properties?.propertyPack?.properties?.ownership?.properties
42
+ ?.ownershipsToBeTransferred?.items?.oneOf?.[2]?.properties
43
+ ?.leaseholdInformation?.properties?.contactDetails;
44
+ const erProp =
45
+ schema.properties?.propertyPack?.properties?.ownership?.properties
46
+ ?.ownershipsToBeTransferred?.items?.oneOf?.[0]?.properties
47
+ ?.estateRentcharges;
48
+
49
+ expect(jkProp?.ntsRef).toBe("A5.3");
50
+ expect(tfProp?.ntsRef).toBe("A3.5.1.1");
51
+ expect(maProp?.ntsRef).toBe("A1.5.4");
52
+ expect(erProp?.ntsRef).toBe("A3.4");
53
+ });
54
+ });
55
+
56
+ describe("Individual extension functionality", () => {
57
+ test("should add Japanese Knotweed extension correctly", () => {
58
+ const schema = getTransactionSchema(schemaId, ["nts2023", "jk"]);
59
+ const jkProp =
60
+ schema.properties?.propertyPack?.properties?.specialistIssues
61
+ ?.properties?.japaneseKnotweed;
62
+
63
+ expect(jkProp).toBeDefined();
64
+
65
+ expect(jkProp.ntsRef).toBe("A5.3");
66
+ expect(jkProp.required).toContain("yesNo");
67
+ expect(jkProp.discriminator?.propertyName).toBe("yesNo");
68
+ });
69
+
70
+ test("should add Transfer Fees extension correctly", () => {
71
+ const schema = getTransactionSchema(schemaId, ["nts2023", "tf"]);
72
+ const tfProp =
73
+ schema.properties?.propertyPack?.properties?.ownership?.properties
74
+ ?.ownershipsToBeTransferred?.items?.oneOf?.[2]?.properties
75
+ ?.leaseholdInformation?.properties?.serviceCharge?.oneOf?.[1]
76
+ ?.properties?.transferFees;
77
+
78
+ expect(tfProp).toBeDefined();
79
+ expect(tfProp.ntsRef).toBe("A3.5.1.1");
80
+ expect(tfProp.title).toBe(
81
+ "Are there any additional fees payable on sale or letting?"
82
+ );
83
+ });
84
+
85
+ test("should add Managing Agent extension correctly", () => {
86
+ const schema = getTransactionSchema(schemaId, ["nts2023", "ma"]);
87
+ const maProp =
88
+ schema.properties?.propertyPack?.properties?.ownership?.properties
89
+ ?.ownershipsToBeTransferred?.items?.oneOf?.[2]?.properties
90
+ ?.leaseholdInformation?.properties?.contactDetails;
91
+
92
+ expect(maProp).toBeDefined();
93
+ expect(maProp.ntsRef).toBe("A1.5.4");
94
+ expect(maProp.required).toContain("contacts");
95
+ });
96
+
97
+ test("should add Solar Panels extension correctly", () => {
98
+ const schema = getTransactionSchema(schemaId, ["nts2023", "sl"]);
99
+ const slProp =
100
+ schema.properties?.propertyPack?.properties?.electricity?.properties
101
+ ?.solarPanels?.oneOf?.[1]?.properties?.panelsOwnedOutright;
102
+
103
+ expect(slProp).toBeDefined();
104
+ expect(slProp.ntsRef).toBe("B3.7.1");
105
+ });
106
+
107
+ test("should add Heating Installation extension correctly", () => {
108
+ const schema = getTransactionSchema(schemaId, ["nts2023", "hi"]);
109
+ const hiProp =
110
+ schema.properties?.propertyPack?.properties?.heating?.properties
111
+ ?.heatingSystem?.oneOf?.[1]?.properties?.centralHeatingDetails
112
+ ?.properties?.centralHeatingInstalled;
113
+
114
+ expect(hiProp).toBeDefined();
115
+ expect(hiProp.ntsRef).toBe("B3.4.3.2");
116
+ });
117
+
118
+ test("should add Estate Rentcharges extension correctly", () => {
119
+ const schema = getTransactionSchema(schemaId, ["nts2023", "er"]);
120
+ const erProp =
121
+ schema.properties?.propertyPack?.properties?.ownership?.properties
122
+ ?.ownershipsToBeTransferred?.items?.oneOf?.[0]?.properties
123
+ ?.estateRentcharges;
124
+
125
+ expect(erProp).toBeDefined();
126
+
127
+ expect(erProp.ntsRef).toBe("A3.4");
128
+ expect(erProp.required).toContain("yesNo");
129
+ expect(erProp.discriminator?.propertyName).toBe("yesNo");
130
+ expect(erProp.oneOf).toBeDefined();
131
+ expect(Array.isArray(erProp.oneOf)).toBe(true);
132
+ expect(erProp.oneOf.length).toBeGreaterThan(0);
133
+
134
+ // Check the conditional branch for "Yes" response
135
+ const yesBranch = erProp.oneOf.find(
136
+ (branch) =>
137
+ branch.required?.includes("details") &&
138
+ branch.required?.includes("amount")
139
+ );
140
+ expect(yesBranch).toBeDefined();
141
+ expect(yesBranch.properties?.details?.ntsRef).toBe("A3.4.2");
142
+ expect(yesBranch.properties?.amount?.ntsRef).toBe("A3.4.3");
143
+ });
144
+ });
145
+
146
+ describe("SEF25 extension leaf annotations", () => {
147
+ test("wg: every W1.x subsection exposes a yesNo ntsRef", () => {
148
+ const schema = getTransactionSchema(schemaId, ["nts2023", "wg"]);
149
+ const subsections =
150
+ schema.properties?.propertyPack?.properties
151
+ ?.guaranteesWarrantiesAndIndemnityInsurances?.oneOf?.[1]?.properties;
152
+
153
+ expect(subsections).toBeDefined();
154
+
155
+ const discriminated = Object.entries(subsections).filter(
156
+ ([, sub]) => sub?.discriminator?.propertyName === "yesNo"
157
+ );
158
+ expect(discriminated.length).toBeGreaterThan(0);
159
+
160
+ for (const [key, sub] of discriminated) {
161
+ expect(sub.ntsRef).toMatch(/^W1\.\d+$/);
162
+ expect(sub.properties?.yesNo?.ntsRef).toMatch(/^W1\.\d+\.1$/);
163
+ expect(sub.required).toContain("yesNo");
164
+ }
165
+ });
166
+
167
+ test("ic: insuranceClaims exposes a yesNo ntsRef", () => {
168
+ const schema = getTransactionSchema(schemaId, ["nts2023", "ic"]);
169
+ const icProp =
170
+ schema.properties?.propertyPack?.properties?.insurance?.oneOf?.[1]
171
+ ?.properties?.insuranceClaims;
172
+
173
+ expect(icProp).toBeDefined();
174
+ expect(icProp.ntsRef).toBe("I1.1");
175
+ expect(icProp.properties?.yesNo?.ntsRef).toBe("I1.1.1");
176
+ expect(icProp.required).toContain("yesNo");
177
+ });
178
+
179
+ test("nd: neighbourDevelopment N1.1 marker merges with nts yesNo ref", () => {
180
+ // nd extension only carries the parent N1.1 marker; the yesNo leaf
181
+ // annotation comes from the nts overlay (C4.1.1), which is required
182
+ // alongside nd for the field to render.
183
+ const schema = getTransactionSchema(schemaId, ["nts2023", "nd"]);
184
+ const ndProp =
185
+ schema.properties?.propertyPack?.properties?.notices?.properties
186
+ ?.neighbourDevelopment;
187
+
188
+ expect(ndProp).toBeDefined();
189
+ expect(ndProp.ntsRef).toBe("N1.1");
190
+ expect(ndProp.properties?.yesNo?.ntsRef).toBe("C4.1.1");
191
+ expect(ndProp.required).toContain("yesNo");
192
+ });
193
+
194
+ test("mi: otherMaterialIssue exposes yesNo and details ntsRef", () => {
195
+ const schema = getTransactionSchema(schemaId, ["nts2023", "mi"]);
196
+ const miProp =
197
+ schema.properties?.propertyPack?.properties?.additionalInformation
198
+ ?.properties?.otherMaterialIssue;
199
+
200
+ expect(miProp).toBeDefined();
201
+ expect(miProp.ntsRef).toBe("M1.1");
202
+ expect(miProp.properties?.yesNo?.ntsRef).toBe("M1.1.1");
203
+ expect(miProp.required).toContain("yesNo");
204
+
205
+ const yesBranch = miProp.oneOf?.find((branch) =>
206
+ branch.required?.includes("details")
207
+ );
208
+ expect(yesBranch).toBeDefined();
209
+ expect(yesBranch.properties?.details?.ntsRef).toBe("M1.1.2");
210
+ });
211
+ });
212
+
213
+ describe("Extension overlay availability", () => {
214
+ test("should export all extension overlays", () => {
215
+ const expectedKeys = [
216
+ "as",
217
+ "dr",
218
+ "jk",
219
+ "sb",
220
+ "hs", // Specialist Issues
221
+ "oa",
222
+ "la",
223
+ "sf",
224
+ "mc", // Property Features
225
+ "er",
226
+ "ma",
227
+ "tf", // Ownership & Financial
228
+ "sl",
229
+ "hi",
230
+ "fd", // Utilities & Services
231
+ "oc", // Transaction
232
+ "sc",
233
+ "pc",
234
+ "ph", // SEF25
235
+ "dk",
236
+ "rw",
237
+ "sd",
238
+ "lc",
239
+ "wg",
240
+ "ic",
241
+ "nd",
242
+ "mi",
243
+ "tr", // SEF25 (new)
244
+ "ac", // SEF25 (new)
245
+ "ta", // Compatibility
246
+ ];
247
+
248
+ expect(Object.keys(extensionOverlays).sort()).toEqual(
249
+ expectedKeys.sort()
250
+ );
251
+ });
252
+
253
+ test("should have valid JSON structure for all extensions", () => {
254
+ Object.entries(extensionOverlays).forEach(([key, overlay]) => {
255
+ expect(overlay).toBeDefined();
256
+ expect(overlay.$schema).toBe("http://json-schema.org/draft-07/schema#");
257
+ expect(overlay.$id).toContain(`extensions/${key}.json`);
258
+ expect(overlay.properties).toBeDefined();
259
+ });
260
+ });
261
+ });
262
+
263
+ describe("TA6 edition 6 compatibility extension", () => {
264
+ test("keeps parking enum strict to TA6 edition 6 when compatibility is applied", () => {
265
+ const ta6Validator = getSubschemaValidator(
266
+ "/propertyPack/parking/controlledParking",
267
+ schemaId,
268
+ ["ta6ed6"]
269
+ );
270
+ expect(ta6Validator({ yesNo: "Not known" })).toBe(false);
271
+
272
+ const compatValidator = getSubschemaValidator(
273
+ "/propertyPack/parking/controlledParking",
274
+ schemaId,
275
+ ["nts2025", "ta"]
276
+ );
277
+ expect(compatValidator({ yesNo: "Not known" })).toBe(false);
278
+ expect(compatValidator({ yesNo: "No" })).toBe(true);
279
+ });
280
+
281
+ test("applies TA6 edition 6 selector values on shared rights and boundaries paths", () => {
282
+ const schema = getTransactionSchema(schemaId, [
283
+ "nts2025",
284
+ "ta",
285
+ ]);
286
+ const propertyPack = schema.properties.propertyPack.properties;
287
+
288
+ expect(
289
+ propertyPack.rightsAndInformalArrangements.properties
290
+ .sharedContributions.oneOf[0].properties.yesNo.enum
291
+ ).toEqual(["No", "Not applicable"]);
292
+
293
+ expect(
294
+ propertyPack.rightsAndInformalArrangements.properties
295
+ .accessRestrictionAttempts.oneOf[0].properties.yesNo.enum
296
+ ).toEqual(["No", "Not known"]);
297
+
298
+ expect(
299
+ propertyPack.legalBoundaries.properties.flyingFreehold.oneOf[0]
300
+ .properties.yesNo.enum
301
+ ).toEqual(["No", "Not known"]);
302
+ });
303
+
304
+ test("applies TA6 edition 6 heating values and report enum when compatibility is applied", () => {
305
+ const heatingFeatures = getSubschemaValidator(
306
+ "/propertyPack/heating/otherHeatingFeatures",
307
+ schemaId,
308
+ ["nts2025", "ta"]
309
+ );
310
+ expect(heatingFeatures(["Mains gas", "LPG", "Biomass"])).toBe(true);
311
+
312
+ const compatReport = getSubschemaValidator(
313
+ "/propertyPack/heating/heatingSystem/centralHeatingDetails/heatingLastServicedReport",
314
+ schemaId,
315
+ ["nts2025", "ta"]
316
+ );
317
+ expect(compatReport("Not available")).toBe(false);
318
+ expect(compatReport("Attached")).toBe(true);
319
+ });
320
+ });
321
+
322
+ describe("NTS2 equivalence test", () => {
323
+ test("should produce equivalent result to nts2 when all relevant extensions are combined", () => {
324
+ // Get the full NTS2 schema
325
+ const nts2Schema = getTransactionSchema(schemaId, ["nts2025"]);
326
+
327
+ // Get NTS base with all relevant extensions
328
+ const allExtensions = [
329
+ "nts2023",
330
+ // Specialist issues extensions
331
+ "as",
332
+ "dr",
333
+ "jk",
334
+ "sb",
335
+ "hs",
336
+ // Property features extensions
337
+ "oa",
338
+ "la",
339
+ "sf",
340
+ "mc",
341
+ // Ownership & financial extensions
342
+ "er",
343
+ "ma",
344
+ "tf",
345
+ // Utilities & services extensions
346
+ "sl",
347
+ "hi",
348
+ "fd",
349
+ // Transaction extensions
350
+ "oc",
351
+ ];
352
+
353
+ const ntsWithAllExtensions = getTransactionSchema(
354
+ schemaId,
355
+ allExtensions
356
+ );
357
+
358
+ // Compare key structural elements
359
+ const nts2PropPack = nts2Schema.properties?.propertyPack;
360
+ const extPropPack = ntsWithAllExtensions.properties?.propertyPack;
361
+
362
+ // Check that specialist issues section exists in both
363
+ expect(nts2PropPack?.properties?.specialistIssues).toBeDefined();
364
+ expect(extPropPack?.properties?.specialistIssues).toBeDefined();
365
+
366
+ // Check key specialist issues properties
367
+ const nts2SI = nts2PropPack.properties.specialistIssues.properties;
368
+ const extSI = extPropPack.properties.specialistIssues.properties;
369
+
370
+ expect(extSI?.japaneseKnotweed).toBeDefined();
371
+ expect(extSI?.dryRotEtcTreatment).toBeDefined();
372
+ expect(extSI?.containsAsbestos).toBeDefined();
373
+ expect(extSI?.subsidenceOrStructuralFault).toBeDefined();
374
+ expect(extSI?.ongoingHealthOrSafetyIssue).toBeDefined();
375
+
376
+ // Check that references match (extensions use ntsRef, NTS2 uses nts2Ref)
377
+ if (nts2SI?.japaneseKnotweed && extSI?.japaneseKnotweed) {
378
+ expect(extSI.japaneseKnotweed.ntsRef).toBe("A5.3");
379
+ // NTS2 might have nts2Ref instead - this is expected difference
380
+ }
381
+
382
+ // Check outside areas
383
+ expect(
384
+ extPropPack?.properties?.residentialPropertyFeatures?.properties
385
+ ?.outsideAreas
386
+ ).toBeDefined();
387
+
388
+ // Check leasehold extensions
389
+ const leaseholdInfo =
390
+ extPropPack?.properties?.ownership?.properties
391
+ ?.ownershipsToBeTransferred?.items?.oneOf?.[2]?.properties
392
+ ?.leaseholdInformation;
393
+ expect(leaseholdInfo?.properties?.contactDetails).toBeDefined(); // Managing agent
394
+ expect(
395
+ leaseholdInfo?.properties?.serviceCharge?.oneOf?.[1]?.properties
396
+ ?.transferFees
397
+ ).toBeDefined(); // Transfer fees
398
+
399
+ // Check utility extensions
400
+ expect(
401
+ extPropPack?.properties?.electricity?.properties?.solarPanels
402
+ ?.oneOf?.[1]?.properties?.panelsOwnedOutright
403
+ ).toBeDefined();
404
+ expect(
405
+ extPropPack?.properties?.heating?.properties?.heatingSystem?.oneOf?.[1]
406
+ ?.properties?.centralHeatingDetails?.properties
407
+ ?.centralHeatingInstalled
408
+ ).toBeDefined();
409
+
410
+ // Check construction extensions
411
+ expect(
412
+ extPropPack?.properties?.typeOfConstruction?.properties?.loft
413
+ ).toBeDefined();
414
+ expect(
415
+ extPropPack?.properties?.typeOfConstruction?.properties
416
+ ?.sprayFoamInsulation
417
+ ).toBeDefined();
418
+
419
+ // Check completion and moving
420
+ expect(
421
+ extPropPack?.properties?.completionAndMoving?.properties
422
+ ?.otherPropertyInChain
423
+ ).toBeDefined();
424
+ });
425
+
426
+ test("should validate data successfully with all extensions", () => {
427
+ const allExtensions = [
428
+ "nts2023",
429
+ "as",
430
+ "dr",
431
+ "jk",
432
+ "sb",
433
+ "hs",
434
+ "oa",
435
+ "la",
436
+ "sf",
437
+ "mc",
438
+ "er",
439
+ "ma",
440
+ "tf",
441
+ "sl",
442
+ "hi",
443
+ "fd",
444
+ "oc",
445
+ ];
446
+
447
+ const validator = getValidator(schemaId, allExtensions);
448
+ expect(validator).toBeDefined();
449
+ expect(typeof validator).toBe("function");
450
+
451
+ // Test with minimal valid data structure
452
+ const testData = {
453
+ propertyPack: {
454
+ priceInformation: { price: 100000, priceQualifier: "Freehold" },
455
+ ownership: { ownershipsToBeTransferred: [] },
456
+ councilTax: { band: "A" },
457
+ energyEfficiency: { epcRating: "C" },
458
+ buildInformation: { building: { buildDate: 2000 } },
459
+ typeOfConstruction: { isStandardForm: { yesNo: "Yes" } },
460
+ electricity: { mainsElectricity: { yesNo: "Yes" } },
461
+ connectivity: { mobilePhoneCoverage: { signal: "Good" } },
462
+ waterAndDrainage: { water: { supplier: "Thames Water" } },
463
+ heating: { heatingSystem: { yesNo: "Yes" } },
464
+ parking: { parkingArrangements: { yesNo: "No" } },
465
+ listingAndConservation: { listedBuilding: { yesNo: "No" } },
466
+ rightsAndInformalArrangements: { rightsGranted: { yesNo: "No" } },
467
+ environmentalIssues: { flooding: { yesNo: "No" } },
468
+ notices: { noticesAndProposals: { yesNo: "No" } },
469
+ specialistIssues: {
470
+ japaneseKnotweed: { yesNo: "No" },
471
+ dryRotEtcTreatment: { yesNo: "No" },
472
+ containsAsbestos: { yesNo: "No" },
473
+ subsidenceOrStructuralFault: { yesNo: "No" },
474
+ ongoingHealthOrSafetyIssue: { yesNo: "No" },
475
+ },
476
+ },
477
+ };
478
+
479
+ // This might not validate due to missing required fields, but validator should not throw
480
+ expect(() => validator(testData)).not.toThrow();
481
+ });
482
+ });
483
+
484
+ describe("Extension merging behavior", () => {
485
+ test("should preserve required arrays when merging extensions", () => {
486
+ const schema = getTransactionSchema(schemaId, ["nts2023", "jk", "as"]);
487
+ const siSection =
488
+ schema.properties?.propertyPack?.properties?.specialistIssues;
489
+
490
+ expect(siSection?.required).toBeDefined();
491
+ expect(siSection.required).toContain("japaneseKnotweed");
492
+ expect(siSection.required).toContain("containsAsbestos");
493
+ });
494
+
495
+ test("should make estateRentcharges required when er extension is applied", () => {
496
+ const schema = getTransactionSchema(schemaId, ["nts2023", "er"]);
497
+
498
+ // Check that estateRentcharges is required at the oneOf branch level
499
+ const freeholdBranch =
500
+ schema.properties?.propertyPack?.properties?.ownership?.properties
501
+ ?.ownershipsToBeTransferred?.items?.oneOf?.[0];
502
+
503
+ expect(freeholdBranch).toBeDefined();
504
+ expect(freeholdBranch.required).toBeDefined();
505
+ expect(freeholdBranch.required).toContain("estateRentcharges");
506
+
507
+ // Verify the estateRentcharges property itself is properly structured
508
+ const erProp = freeholdBranch.properties?.estateRentcharges;
509
+ expect(erProp).toBeDefined();
510
+ expect(erProp.ntsRef).toBe("A3.4");
511
+ expect(erProp.required).toContain("yesNo");
512
+ });
513
+
514
+ test("should consistently apply required fields at parent level for extension properties", () => {
515
+ // Test multiple extensions that should have required fields at parent level
516
+ const schema = getTransactionSchema(schemaId, [
517
+ "nts2023",
518
+ "jk",
519
+ "er",
520
+ "ma",
521
+ "tf",
522
+ ]);
523
+
524
+ // Check specialist issues - japaneseKnotweed should be required
525
+ const siSection =
526
+ schema.properties?.propertyPack?.properties?.specialistIssues;
527
+ expect(siSection?.required).toContain("japaneseKnotweed");
528
+
529
+ // Check ownership - estateRentcharges should be required in freehold branch
530
+ const freeholdBranch =
531
+ schema.properties?.propertyPack?.properties?.ownership?.properties
532
+ ?.ownershipsToBeTransferred?.items?.oneOf?.[0];
533
+ expect(freeholdBranch?.required).toContain("estateRentcharges");
534
+
535
+ // Check leasehold - leaseholdInformation should be required in leasehold branch
536
+ const leaseholdBranch =
537
+ schema.properties?.propertyPack?.properties?.ownership?.properties
538
+ ?.ownershipsToBeTransferred?.items?.oneOf?.[2];
539
+ expect(leaseholdBranch?.required).toContain("leaseholdInformation");
540
+
541
+ // Verify that the required arrays are properly merged (not overwritten)
542
+ // Note: The length check might fail if only one extension is applied to specialistIssues
543
+ // Let's check what's actually in the required arrays
544
+ console.log("SpecialistIssues required:", siSection?.required);
545
+ console.log("FreeholdBranch required:", freeholdBranch?.required);
546
+ console.log("LeaseholdBranch required:", leaseholdBranch?.required);
547
+
548
+ expect(siSection.required.length).toBeGreaterThanOrEqual(1);
549
+ expect(freeholdBranch.required.length).toBeGreaterThan(0);
550
+ expect(leaseholdBranch.required.length).toBeGreaterThan(0);
551
+ });
552
+
553
+ test("should merge required arrays for multiple specialist issues overlays", () => {
554
+ // Apply multiple specialist issues overlays
555
+ const overlays = ["nts2023", "jk", "as", "dr", "sb", "hs"];
556
+ const schema = getTransactionSchema(schemaId, overlays);
557
+ const siSection =
558
+ schema.properties?.propertyPack?.properties?.specialistIssues;
559
+
560
+ // The overlays correspond to these property keys
561
+ const expectedRequired = [
562
+ "japaneseKnotweed",
563
+ "containsAsbestos",
564
+ "dryRotEtcTreatment",
565
+ "subsidenceOrStructuralFault",
566
+ "ongoingHealthOrSafetyIssue",
567
+ ];
568
+
569
+ expect(siSection?.required).toBeDefined();
570
+ // Should contain all expected keys
571
+ expectedRequired.forEach((key) => {
572
+ expect(siSection.required).toContain(key);
573
+ });
574
+ // Should be the same length as the number of overlays (excluding nts2023)
575
+ expect(siSection.required.length).toBe(expectedRequired.length);
576
+ });
577
+
578
+ test("should handle discriminator patterns correctly", () => {
579
+ const schema = getTransactionSchema(schemaId, ["nts2023", "jk"]);
580
+ const jkProp =
581
+ schema.properties?.propertyPack?.properties?.specialistIssues
582
+ ?.properties?.japaneseKnotweed;
583
+
584
+ expect(jkProp?.discriminator).toBeDefined();
585
+ expect(jkProp.discriminator.propertyName).toBe("yesNo");
586
+ expect(jkProp.oneOf).toBeDefined();
587
+ expect(Array.isArray(jkProp.oneOf)).toBe(true);
588
+ });
589
+
590
+ test("should merge oneOf schemas correctly", () => {
591
+ const schema = getTransactionSchema(schemaId, ["nts2023", "tf"]);
592
+ const tfProp =
593
+ schema.properties?.propertyPack?.properties?.ownership?.properties
594
+ ?.ownershipsToBeTransferred?.items?.oneOf?.[2]?.properties
595
+ ?.leaseholdInformation?.properties?.serviceCharge?.oneOf?.[1]
596
+ ?.properties?.transferFees;
597
+
598
+ expect(tfProp?.oneOf).toBeDefined();
599
+ expect(tfProp.oneOf.length).toBeGreaterThan(0);
600
+
601
+ // Check the structure - details should be in the conditional oneOf branch
602
+ const conditionalBranch = tfProp.oneOf.find(
603
+ (branch) =>
604
+ branch.properties?.details || branch.required?.includes("details")
605
+ );
606
+ expect(conditionalBranch).toBeDefined();
607
+ expect(
608
+ conditionalBranch.properties?.details ||
609
+ conditionalBranch.required?.includes("details")
610
+ ).toBeTruthy();
611
+ });
612
+
613
+ test("should merge supply costs extension into mainsWater No branch", () => {
614
+ const schema = getTransactionSchema(schemaId, ["sc"]);
615
+ const mainsWater =
616
+ schema.properties?.propertyPack?.properties?.waterAndDrainage
617
+ ?.properties?.water?.properties?.mainsWater;
618
+
619
+ expect(mainsWater?.oneOf).toBeDefined();
620
+
621
+ // Branch 0 is yesNo="No" — should now contain associatedCost
622
+ const noBranch = mainsWater.oneOf[0];
623
+ const cost = noBranch?.properties?.associatedCost;
624
+ expect(cost).toBeDefined();
625
+ expect(cost.ntsRef).toBe("U1.1");
626
+ expect(cost.required).toEqual(["frequency"]);
627
+ expect(cost.properties.frequency.type).toBe("string");
628
+ // amount now lives inside the frequency-discriminator's "Per month"/"Per year" branch
629
+ const costBranch = cost.oneOf?.find((b) =>
630
+ b.properties?.frequency?.enum?.includes("Per month")
631
+ );
632
+ expect(costBranch?.properties?.amount?.type).toBe("number");
633
+ expect(costBranch?.required).toEqual(["amount"]);
634
+ });
635
+
636
+ test("should merge supply costs extension into mainsFoulDrainage No/Not known branch", () => {
637
+ const schema = getTransactionSchema(schemaId, ["sc"]);
638
+ const mainsFoul =
639
+ schema.properties?.propertyPack?.properties?.waterAndDrainage
640
+ ?.properties?.drainage?.properties?.mainsFoulDrainage;
641
+
642
+ expect(mainsFoul?.oneOf).toBeDefined();
643
+
644
+ // Branch 2 is yesNo="No"/"Not known" — should now contain associatedCost
645
+ const noBranch = mainsFoul.oneOf[2];
646
+ const cost = noBranch?.properties?.associatedCost;
647
+ expect(cost).toBeDefined();
648
+ expect(cost.ntsRef).toBe("U1.2");
649
+ expect(cost.required).toEqual(["frequency"]);
650
+ // amount now lives inside the frequency-discriminator's "Per month"/"Per year" branch
651
+ const costBranch = cost.oneOf?.find((b) =>
652
+ b.properties?.frequency?.enum?.includes("Per month")
653
+ );
654
+ expect(costBranch?.properties?.amount?.type).toBe("number");
655
+ expect(costBranch?.required).toEqual(["amount"]);
656
+ });
657
+
658
+ test("should merge parking permit cost and frequency into controlledParking Yes branch", () => {
659
+ const schema = getTransactionSchema(schemaId, ["pc"]);
660
+ const controlled =
661
+ schema.properties?.propertyPack?.properties?.parking?.properties
662
+ ?.controlledParking;
663
+
664
+ expect(controlled?.oneOf).toBeDefined();
665
+
666
+ // Branch 1 is yesNo="Yes"
667
+ const yesBranch = controlled.oneOf[1];
668
+ const cost = yesBranch?.properties?.costOfPermit;
669
+ expect(cost).toBeDefined();
670
+ expect(cost.ntsRef).toBe("P1.1");
671
+ expect(cost.type).toBe("number");
672
+
673
+ const freq = yesBranch?.properties?.costOfPermitFrequency;
674
+ expect(freq).toBeDefined();
675
+ expect(freq.ntsRef).toBe("P1.2");
676
+ expect(freq.type).toBe("string");
677
+ });
678
+
679
+ test("should merge all 4 property hazard fields into specialistIssues", () => {
680
+ const schema = getTransactionSchema(schemaId, ["ph"]);
681
+ const si =
682
+ schema.properties?.propertyPack?.properties?.specialistIssues
683
+ ?.properties;
684
+
685
+ const expectedFields = [
686
+ { key: "wellsDitchesShaft", ref: "H1.1" },
687
+ { key: "damagedOrExposedElectrics", ref: "H1.2" },
688
+ { key: "damageToFlooringOrStaircases", ref: "H1.3" },
689
+ { key: "knownAreasInPoorCondition", ref: "H1.4" },
690
+ ];
691
+
692
+ expectedFields.forEach(({ key, ref }) => {
693
+ expect(si[key]).toBeDefined();
694
+ expect(si[key].ntsRef).toBe(ref);
695
+ expect(si[key].required).toContain("yesNo");
696
+ expect(si[key].discriminator?.propertyName).toBe("yesNo");
697
+ expect(si[key].oneOf).toHaveLength(2);
698
+
699
+ // "No" branch should not require details
700
+ const noBranch = si[key].oneOf.find((b) =>
701
+ b.properties?.yesNo?.enum?.includes("No")
702
+ );
703
+ expect(noBranch).toBeDefined();
704
+ expect(noBranch.required).toBeUndefined();
705
+
706
+ // "Yes" branch should require details
707
+ const yesBranch = si[key].oneOf.find((b) =>
708
+ b.properties?.yesNo?.enum?.includes("Yes")
709
+ );
710
+ expect(yesBranch).toBeDefined();
711
+ expect(yesBranch.required).toContain("details");
712
+ expect(yesBranch.properties.details.minLength).toBe(1);
713
+ });
714
+ });
715
+
716
+ test("should not require additional fields when 'No' is selected in extension overlays", () => {
717
+ // Test the 'as' (additional searches) extension overlay
718
+ const schema = getTransactionSchema(schemaId, ["as"]);
719
+
720
+ const containsAsbestos =
721
+ schema.properties?.propertyPack?.properties?.specialistIssues
722
+ ?.properties?.containsAsbestos;
723
+
724
+ expect(containsAsbestos).toBeDefined();
725
+ expect(containsAsbestos.discriminator?.propertyName).toBe("yesNo");
726
+ expect(containsAsbestos.oneOf).toBeDefined();
727
+ expect(containsAsbestos.oneOf).toHaveLength(2); // Both "No" and "Yes" branches
728
+
729
+ // Find the "No" branch - it should not have additional required fields
730
+ const noBranch = containsAsbestos.oneOf.find((branch) =>
731
+ branch.properties?.yesNo?.enum?.includes("No")
732
+ );
733
+ expect(noBranch).toBeDefined();
734
+ expect(noBranch.properties.yesNo.enum).toEqual(["No"]);
735
+ // The "No" branch should not have additional required fields beyond "yesNo"
736
+ expect(noBranch.required).toBeUndefined();
737
+
738
+ // Find the "Yes" branch - it should have additional required fields
739
+ const yesBranch = containsAsbestos.oneOf.find((branch) =>
740
+ branch.properties?.yesNo?.enum?.includes("Yes")
741
+ );
742
+ expect(yesBranch).toBeDefined();
743
+ expect(yesBranch.properties.yesNo.enum).toEqual(["Yes"]);
744
+ expect(yesBranch.required).toBeDefined();
745
+ expect(yesBranch.required).toContain("details");
746
+
747
+ // Test that data with "No" answer validates correctly using subschema validation
748
+ const { getSubschemaValidator } = require("../../../index.js");
749
+ const subschemaValidator = getSubschemaValidator(
750
+ "/propertyPack/specialistIssues/containsAsbestos",
751
+ schemaId,
752
+ ["as"]
753
+ );
754
+
755
+ const validDataWithNo = {
756
+ yesNo: "No",
757
+ };
758
+
759
+ const result = subschemaValidator(validDataWithNo);
760
+ // Handle both boolean and object return types
761
+ if (typeof result === "boolean") {
762
+ expect(result).toBe(true);
763
+ } else {
764
+ expect(result.valid).toBe(true);
765
+ expect(result.errors).toHaveLength(0);
766
+ }
767
+ });
768
+ });
769
+
770
+ describe("SEF25 extension validation", () => {
771
+ describe("Supply costs (sc)", () => {
772
+ const overlays = ["baspiV5", "sc"];
773
+
774
+ test("should validate valid private water cost", () => {
775
+ const validator = getSubschemaValidator(
776
+ "/propertyPack/waterAndDrainage/water/mainsWater",
777
+ schemaId,
778
+ overlays
779
+ );
780
+
781
+ const result = validator({
782
+ yesNo: "No",
783
+ details: "Private borehole",
784
+ associatedCost: {
785
+ amount: 50,
786
+ frequency: "Per month",
787
+ },
788
+ });
789
+
790
+ expect(result).toBe(true);
791
+ });
792
+
793
+ test("should validate private water with no associated costs", () => {
794
+ const validator = getSubschemaValidator(
795
+ "/propertyPack/waterAndDrainage/water/mainsWater",
796
+ schemaId,
797
+ overlays
798
+ );
799
+
800
+ const result = validator({
801
+ yesNo: "No",
802
+ details: "Private borehole",
803
+ associatedCost: {
804
+ amount: 0,
805
+ frequency: "Not applicable",
806
+ },
807
+ });
808
+
809
+ expect(result).toBe(true);
810
+ });
811
+
812
+ test("should require associated cost for private water supply", () => {
813
+ const validator = getSubschemaValidator(
814
+ "/propertyPack/waterAndDrainage/water/mainsWater",
815
+ schemaId,
816
+ overlays
817
+ );
818
+
819
+ const result = validator({
820
+ yesNo: "No",
821
+ details: "Private well",
822
+ });
823
+
824
+ // associatedCost is required by the sc extension
825
+ expect(result).toBe(false);
826
+ });
827
+
828
+ test("should reject invalid frequency value for water cost", () => {
829
+ const validator = getSubschemaValidator(
830
+ "/propertyPack/waterAndDrainage/water/mainsWater",
831
+ schemaId,
832
+ overlays
833
+ );
834
+
835
+ const result = validator({
836
+ yesNo: "No",
837
+ details: "Borehole",
838
+ associatedCost: {
839
+ amount: 100,
840
+ frequency: "Weekly",
841
+ },
842
+ });
843
+
844
+ expect(result).toBe(false);
845
+ });
846
+
847
+ test("should reject non-numeric amount for water cost", () => {
848
+ const validator = getSubschemaValidator(
849
+ "/propertyPack/waterAndDrainage/water/mainsWater",
850
+ schemaId,
851
+ overlays
852
+ );
853
+
854
+ const result = validator({
855
+ yesNo: "No",
856
+ details: "Borehole",
857
+ associatedCost: {
858
+ amount: "fifty pounds",
859
+ frequency: "Per month",
860
+ },
861
+ });
862
+
863
+ expect(result).toBe(false);
864
+ });
865
+
866
+ test("should validate valid private sewerage cost", () => {
867
+ // Use sc without baspiV5 to avoid strict offMainsDrainageSystem requirements
868
+ const validator = getSubschemaValidator(
869
+ "/propertyPack/waterAndDrainage/drainage/mainsFoulDrainage",
870
+ schemaId,
871
+ ["sc"]
872
+ );
873
+
874
+ const result = validator({
875
+ yesNo: "No",
876
+ associatedCost: {
877
+ amount: 200,
878
+ frequency: "Per year",
879
+ },
880
+ });
881
+
882
+ expect(result).toBe(true);
883
+ });
884
+
885
+ test("should reject invalid frequency for sewerage cost", () => {
886
+ const validator = getSubschemaValidator(
887
+ "/propertyPack/waterAndDrainage/drainage/mainsFoulDrainage",
888
+ schemaId,
889
+ ["sc"]
890
+ );
891
+
892
+ const result = validator({
893
+ yesNo: "No",
894
+ associatedCost: {
895
+ amount: 200,
896
+ frequency: "Quarterly",
897
+ },
898
+ });
899
+
900
+ expect(result).toBe(false);
901
+ });
902
+
903
+ test("should not allow associated cost when mains water is Yes", () => {
904
+ const validator = getSubschemaValidator(
905
+ "/propertyPack/waterAndDrainage/water/mainsWater",
906
+ schemaId,
907
+ overlays
908
+ );
909
+
910
+ // When yesNo="Yes", associatedCost should cause validation failure
911
+ // because it's only defined in the "No" oneOf branch
912
+ const result = validator({
913
+ yesNo: "Yes",
914
+ supplier: "Thames Water",
915
+ associatedCost: {
916
+ amount: 50,
917
+ frequency: "Per month",
918
+ },
919
+ });
920
+
921
+ expect(result).toBe(false);
922
+ });
923
+ });
924
+
925
+ describe("Parking permit cost (pc)", () => {
926
+ const overlays = ["baspiV5", "pc"];
927
+
928
+ test("should validate valid parking permit with frequency", () => {
929
+ const validator = getSubschemaValidator(
930
+ "/propertyPack/parking/controlledParking",
931
+ schemaId,
932
+ overlays
933
+ );
934
+
935
+ const result = validator({
936
+ yesNo: "Yes",
937
+ costOfPermit: 150,
938
+ costOfPermitFrequency: "Per year",
939
+ });
940
+
941
+ expect(result).toBe(true);
942
+ });
943
+
944
+ test("should validate parking permit with monthly frequency", () => {
945
+ const validator = getSubschemaValidator(
946
+ "/propertyPack/parking/controlledParking",
947
+ schemaId,
948
+ overlays
949
+ );
950
+
951
+ const result = validator({
952
+ yesNo: "Yes",
953
+ costOfPermit: 12.50,
954
+ costOfPermitFrequency: "Per month",
955
+ });
956
+
957
+ expect(result).toBe(true);
958
+ });
959
+
960
+ test("should reject invalid frequency for parking permit", () => {
961
+ const validator = getSubschemaValidator(
962
+ "/propertyPack/parking/controlledParking",
963
+ schemaId,
964
+ overlays
965
+ );
966
+
967
+ const result = validator({
968
+ yesNo: "Yes",
969
+ costOfPermit: 150,
970
+ costOfPermitFrequency: "Per quarter",
971
+ });
972
+
973
+ expect(result).toBe(false);
974
+ });
975
+
976
+ test("should validate No answer without cost fields", () => {
977
+ const validator = getSubschemaValidator(
978
+ "/propertyPack/parking/controlledParking",
979
+ schemaId,
980
+ overlays
981
+ );
982
+
983
+ const result = validator({
984
+ yesNo: "No",
985
+ });
986
+
987
+ expect(result).toBe(true);
988
+ });
989
+ });
990
+
991
+ describe("Property hazards (ph)", () => {
992
+ const overlays = ["ph"];
993
+ const hazardFields = [
994
+ "wellsDitchesShaft",
995
+ "damagedOrExposedElectrics",
996
+ "damageToFlooringOrStaircases",
997
+ "knownAreasInPoorCondition",
998
+ ];
999
+
1000
+ hazardFields.forEach((field) => {
1001
+ test(`${field}: should validate "No" answer`, () => {
1002
+ const validator = getSubschemaValidator(
1003
+ `/propertyPack/specialistIssues/${field}`,
1004
+ schemaId,
1005
+ overlays
1006
+ );
1007
+
1008
+ const result = validator({ yesNo: "No" });
1009
+ expect(result).toBe(true);
1010
+ });
1011
+
1012
+ test(`${field}: should validate "Yes" with details`, () => {
1013
+ const validator = getSubschemaValidator(
1014
+ `/propertyPack/specialistIssues/${field}`,
1015
+ schemaId,
1016
+ overlays
1017
+ );
1018
+
1019
+ const result = validator({
1020
+ yesNo: "Yes",
1021
+ details: "Some description of the issue",
1022
+ });
1023
+ expect(result).toBe(true);
1024
+ });
1025
+
1026
+ test(`${field}: should reject "Yes" without details`, () => {
1027
+ const validator = getSubschemaValidator(
1028
+ `/propertyPack/specialistIssues/${field}`,
1029
+ schemaId,
1030
+ overlays
1031
+ );
1032
+
1033
+ const result = validator({ yesNo: "Yes" });
1034
+ expect(result).toBe(false);
1035
+ });
1036
+
1037
+ test(`${field}: should reject "Yes" with empty details`, () => {
1038
+ const validator = getSubschemaValidator(
1039
+ `/propertyPack/specialistIssues/${field}`,
1040
+ schemaId,
1041
+ overlays
1042
+ );
1043
+
1044
+ const result = validator({ yesNo: "Yes", details: "" });
1045
+ expect(result).toBe(false);
1046
+ });
1047
+
1048
+ test(`${field}: should reject invalid yesNo value`, () => {
1049
+ const validator = getSubschemaValidator(
1050
+ `/propertyPack/specialistIssues/${field}`,
1051
+ schemaId,
1052
+ overlays
1053
+ );
1054
+
1055
+ const result = validator({ yesNo: "Maybe" });
1056
+ expect(result).toBe(false);
1057
+ });
1058
+ });
1059
+ });
1060
+
1061
+ describe("Combined SEF25 extensions", () => {
1062
+ test("should load all three SEF25 extensions together", () => {
1063
+ const schema = getTransactionSchema(schemaId, [
1064
+ "baspiV5",
1065
+ "sc",
1066
+ "pc",
1067
+ "ph",
1068
+ ]);
1069
+
1070
+ // Supply costs
1071
+ const waterCost =
1072
+ schema.properties?.propertyPack?.properties?.waterAndDrainage
1073
+ ?.properties?.water?.properties?.mainsWater?.oneOf?.[0]?.properties
1074
+ ?.associatedCost;
1075
+ expect(waterCost).toBeDefined();
1076
+
1077
+ const sewerageCost =
1078
+ schema.properties?.propertyPack?.properties?.waterAndDrainage
1079
+ ?.properties?.drainage?.properties?.mainsFoulDrainage?.oneOf?.[2]
1080
+ ?.properties?.associatedCost;
1081
+ expect(sewerageCost).toBeDefined();
1082
+
1083
+ // Parking
1084
+ const parkingFreq =
1085
+ schema.properties?.propertyPack?.properties?.parking?.properties
1086
+ ?.controlledParking?.oneOf?.[1]?.properties?.costOfPermitFrequency;
1087
+ expect(parkingFreq).toBeDefined();
1088
+
1089
+ // Hazards
1090
+ const si =
1091
+ schema.properties?.propertyPack?.properties?.specialistIssues
1092
+ ?.properties;
1093
+ expect(si?.wellsDitchesShaft).toBeDefined();
1094
+ expect(si?.damagedOrExposedElectrics).toBeDefined();
1095
+ expect(si?.damageToFlooringOrStaircases).toBeDefined();
1096
+ expect(si?.knownAreasInPoorCondition).toBeDefined();
1097
+ });
1098
+
1099
+ test("should not interfere with existing NTS2 extensions", () => {
1100
+ const schema = getTransactionSchema(schemaId, [
1101
+ "nts2023",
1102
+ "jk",
1103
+ "hs",
1104
+ "sc",
1105
+ "pc",
1106
+ "ph",
1107
+ ]);
1108
+
1109
+ const si =
1110
+ schema.properties?.propertyPack?.properties?.specialistIssues;
1111
+
1112
+ // Existing NTS2 extensions still work
1113
+ expect(si?.properties?.japaneseKnotweed?.ntsRef).toBe("A5.3");
1114
+ expect(si?.properties?.ongoingHealthOrSafetyIssue?.ntsRef).toBe(
1115
+ "A5.5"
1116
+ );
1117
+ expect(si?.required).toContain("japaneseKnotweed");
1118
+ expect(si?.required).toContain("ongoingHealthOrSafetyIssue");
1119
+
1120
+ // SEF25 hazards also present (remapped to ntsRef in extension overlays)
1121
+ expect(si?.properties?.wellsDitchesShaft?.ntsRef).toBe("H1.1");
1122
+ expect(si?.properties?.knownAreasInPoorCondition?.ntsRef).toBe(
1123
+ "H1.4"
1124
+ );
1125
+ });
1126
+ });
1127
+
1128
+ describe("Dropped kerb (dk)", () => {
1129
+ test("should validate Yes answer", () => {
1130
+ const validator = getSubschemaValidator(
1131
+ "/propertyPack/parking/droppedKerbAccess",
1132
+ schemaId,
1133
+ ["dk"]
1134
+ );
1135
+ expect(validator({ yesNo: "Yes" })).toBe(true);
1136
+ });
1137
+
1138
+ test("should validate No answer", () => {
1139
+ const validator = getSubschemaValidator(
1140
+ "/propertyPack/parking/droppedKerbAccess",
1141
+ schemaId,
1142
+ ["dk"]
1143
+ );
1144
+ expect(validator({ yesNo: "No" })).toBe(true);
1145
+ });
1146
+
1147
+ test("should reject invalid value", () => {
1148
+ const validator = getSubschemaValidator(
1149
+ "/propertyPack/parking/droppedKerbAccess",
1150
+ schemaId,
1151
+ ["dk"]
1152
+ );
1153
+ expect(validator({ yesNo: "Maybe" })).toBe(false);
1154
+ });
1155
+
1156
+ test("should not be required without dk overlay", () => {
1157
+ const schema = getTransactionSchema(schemaId, ["baspiV5"]);
1158
+ const parking = schema.properties?.propertyPack?.properties?.parking;
1159
+ expect(parking?.required || []).not.toContain("droppedKerbAccess");
1160
+ });
1161
+
1162
+ test("should be required with dk overlay", () => {
1163
+ const schema = getTransactionSchema(schemaId, ["dk"]);
1164
+ const parking = schema.properties?.propertyPack?.properties?.parking;
1165
+ expect(parking?.required).toContain("droppedKerbAccess");
1166
+ });
1167
+ });
1168
+
1169
+ describe("Private right of way (rw)", () => {
1170
+ test("should validate No answer", () => {
1171
+ const validator = getSubschemaValidator(
1172
+ "/propertyPack/rightsAndInformalArrangements/rightsOrArrangements/privateRightOfWay",
1173
+ schemaId,
1174
+ ["rw"]
1175
+ );
1176
+ expect(validator({ yesNo: "No" })).toBe(true);
1177
+ });
1178
+
1179
+ test("should validate Yes with details", () => {
1180
+ const validator = getSubschemaValidator(
1181
+ "/propertyPack/rightsAndInformalArrangements/rightsOrArrangements/privateRightOfWay",
1182
+ schemaId,
1183
+ ["rw"]
1184
+ );
1185
+ expect(validator({ yesNo: "Yes", details: "Neighbour has access through garden" })).toBe(true);
1186
+ });
1187
+
1188
+ test("should reject Yes without details", () => {
1189
+ const validator = getSubschemaValidator(
1190
+ "/propertyPack/rightsAndInformalArrangements/rightsOrArrangements/privateRightOfWay",
1191
+ schemaId,
1192
+ ["rw"]
1193
+ );
1194
+ expect(validator({ yesNo: "Yes" })).toBe(false);
1195
+ });
1196
+
1197
+ test("should reject Yes with empty details", () => {
1198
+ const validator = getSubschemaValidator(
1199
+ "/propertyPack/rightsAndInformalArrangements/rightsOrArrangements/privateRightOfWay",
1200
+ schemaId,
1201
+ ["rw"]
1202
+ );
1203
+ expect(validator({ yesNo: "Yes", details: "" })).toBe(false);
1204
+ });
1205
+
1206
+ test("should not be required without rw overlay", () => {
1207
+ const schema = getTransactionSchema(schemaId, ["baspiV5"]);
1208
+ const rights = schema.properties?.propertyPack?.properties?.rightsAndInformalArrangements
1209
+ ?.properties?.rightsOrArrangements;
1210
+ expect(rights?.required || []).not.toContain("privateRightOfWay");
1211
+ });
1212
+
1213
+ test("should be required with rw overlay", () => {
1214
+ const schema = getTransactionSchema(schemaId, ["rw"]);
1215
+ const rights = schema.properties?.propertyPack?.properties?.rightsAndInformalArrangements
1216
+ ?.properties?.rightsOrArrangements;
1217
+ expect(rights?.required).toContain("privateRightOfWay");
1218
+ });
1219
+ });
1220
+
1221
+ describe("Storm/fire/flood damage (sd)", () => {
1222
+ test("should validate No answer", () => {
1223
+ const validator = getSubschemaValidator(
1224
+ "/propertyPack/environmentalIssues/stormFireFloodDamage",
1225
+ schemaId,
1226
+ ["sd"]
1227
+ );
1228
+ expect(validator({ yesNo: "No" })).toBe(true);
1229
+ });
1230
+
1231
+ test("should validate Yes with details", () => {
1232
+ const validator = getSubschemaValidator(
1233
+ "/propertyPack/environmentalIssues/stormFireFloodDamage",
1234
+ schemaId,
1235
+ ["sd"]
1236
+ );
1237
+ expect(validator({ yesNo: "Yes", details: "Flood damage in 2020" })).toBe(true);
1238
+ });
1239
+
1240
+ test("should reject Yes without details", () => {
1241
+ const validator = getSubschemaValidator(
1242
+ "/propertyPack/environmentalIssues/stormFireFloodDamage",
1243
+ schemaId,
1244
+ ["sd"]
1245
+ );
1246
+ expect(validator({ yesNo: "Yes" })).toBe(false);
1247
+ });
1248
+
1249
+ test("should not be required without sd overlay", () => {
1250
+ const schema = getTransactionSchema(schemaId, ["baspiV5"]);
1251
+ const env = schema.properties?.propertyPack?.properties?.environmentalIssues;
1252
+ expect(env?.required || []).not.toContain("stormFireFloodDamage");
1253
+ });
1254
+
1255
+ test("should be required with sd overlay", () => {
1256
+ const schema = getTransactionSchema(schemaId, ["sd"]);
1257
+ const env = schema.properties?.propertyPack?.properties?.environmentalIssues;
1258
+ expect(env?.required).toContain("stormFireFloodDamage");
1259
+ });
1260
+ });
1261
+
1262
+ describe("Solar lease costs (lc)", () => {
1263
+ test("should validate Not applicable frequency", () => {
1264
+ const validator = getSubschemaValidator(
1265
+ "/propertyPack/electricity/solarPanels/panelsOwnedOutright/associatedCost",
1266
+ schemaId,
1267
+ ["lc"]
1268
+ );
1269
+ expect(validator({ amount: 0, frequency: "Not applicable" })).toBe(true);
1270
+ });
1271
+
1272
+ test("should validate amount and frequency", () => {
1273
+ const validator = getSubschemaValidator(
1274
+ "/propertyPack/electricity/solarPanels/panelsOwnedOutright/associatedCost",
1275
+ schemaId,
1276
+ ["lc"]
1277
+ );
1278
+ expect(validator({ amount: 75, frequency: "Per month" })).toBe(true);
1279
+ });
1280
+
1281
+ test("should reject missing amount", () => {
1282
+ const validator = getSubschemaValidator(
1283
+ "/propertyPack/electricity/solarPanels/panelsOwnedOutright/associatedCost",
1284
+ schemaId,
1285
+ ["lc"]
1286
+ );
1287
+ expect(validator({ frequency: "Per month" })).toBe(false);
1288
+ });
1289
+
1290
+ test("should reject missing frequency", () => {
1291
+ const validator = getSubschemaValidator(
1292
+ "/propertyPack/electricity/solarPanels/panelsOwnedOutright/associatedCost",
1293
+ schemaId,
1294
+ ["lc"]
1295
+ );
1296
+ expect(validator({ amount: 75 })).toBe(false);
1297
+ });
1298
+
1299
+ test("should reject invalid frequency", () => {
1300
+ const validator = getSubschemaValidator(
1301
+ "/propertyPack/electricity/solarPanels/panelsOwnedOutright/associatedCost",
1302
+ schemaId,
1303
+ ["lc"]
1304
+ );
1305
+ expect(validator({ amount: 75, frequency: "Weekly" })).toBe(false);
1306
+ });
1307
+ });
1308
+
1309
+ describe("Warranties and guarantees (wg)", () => {
1310
+ const warrantyCategoryPaths = [
1311
+ { path: "newHomeWarranty", ref: "W1.1" },
1312
+ { path: "roofingWork", ref: "W1.2" },
1313
+ { path: "dampProofingTreatment", ref: "W1.3" },
1314
+ { path: "centralHeatingAndorPlumbing", ref: "W1.4" },
1315
+ { path: "electricalRepairOrInstallation", ref: "W1.5" },
1316
+ { path: "subsidenceWork", ref: "W1.6" },
1317
+ { path: "otherGuarantees", ref: "W1.7" },
1318
+ ];
1319
+
1320
+ test("should have ntsRef on all warranty categories when wg overlay loaded", () => {
1321
+ const schema = getTransactionSchema(schemaId, ["baspiV5", "wg"]);
1322
+ const warranties =
1323
+ schema.properties?.propertyPack?.properties?.guaranteesWarrantiesAndIndemnityInsurances;
1324
+
1325
+ // Warranty fields are in oneOf[1] (hasValidGuaranteesOrWarranties = Yes)
1326
+ const yesBranch = warranties.oneOf[1];
1327
+
1328
+ warrantyCategoryPaths.forEach(({ path, ref }) => {
1329
+ expect(yesBranch.properties[path]?.ntsRef).toBe(ref);
1330
+ });
1331
+ });
1332
+
1333
+ test("should mark warranty categories as required when wg overlay loaded", () => {
1334
+ const schema = getTransactionSchema(schemaId, ["baspiV5", "wg"]);
1335
+ const warranties =
1336
+ schema.properties?.propertyPack?.properties?.guaranteesWarrantiesAndIndemnityInsurances;
1337
+
1338
+ const yesBranch = warranties.oneOf[1];
1339
+ warrantyCategoryPaths.forEach(({ path }) => {
1340
+ expect(yesBranch.required).toContain(path);
1341
+ });
1342
+ });
1343
+
1344
+ test("should not require warranty categories without any overlay", () => {
1345
+ const schema = getTransactionSchema(schemaId, []);
1346
+ const warranties =
1347
+ schema.properties?.propertyPack?.properties?.guaranteesWarrantiesAndIndemnityInsurances;
1348
+
1349
+ const yesBranch = warranties.oneOf[1];
1350
+ // Without any overlay, warranty categories should not be in required
1351
+ expect(yesBranch.required || []).not.toContain("newHomeWarranty");
1352
+ expect(yesBranch.required || []).not.toContain("electricalRepairOrInstallation");
1353
+ });
1354
+ });
1355
+
1356
+ describe("Insurance claims (ic)", () => {
1357
+ test("should mark insuranceClaims as required in isInsured=Yes branch", () => {
1358
+ const schema = getTransactionSchema(schemaId, ["ic"]);
1359
+ const insurance = schema.properties?.propertyPack?.properties?.insurance;
1360
+ // oneOf[1] is isInsured=Yes
1361
+ const yesBranch = insurance.oneOf[1];
1362
+ expect(yesBranch?.required).toContain("insuranceClaims");
1363
+ });
1364
+
1365
+ test("should validate insuranceClaims No answer", () => {
1366
+ const validator = getSubschemaValidator(
1367
+ "/propertyPack/insurance/insuranceClaims",
1368
+ schemaId,
1369
+ ["baspiV5", "ic"]
1370
+ );
1371
+ expect(validator({ yesNo: "No" })).toBe(true);
1372
+ });
1373
+
1374
+ test("should validate insuranceClaims Yes with details", () => {
1375
+ const validator = getSubschemaValidator(
1376
+ "/propertyPack/insurance/insuranceClaims",
1377
+ schemaId,
1378
+ ["baspiV5", "ic"]
1379
+ );
1380
+ expect(validator({ yesNo: "Yes", details: "Storm damage claim in 2019" })).toBe(true);
1381
+ });
1382
+ });
1383
+
1384
+ describe("Neighbour development (nd)", () => {
1385
+ test("should add ntsRef to neighbourDevelopment", () => {
1386
+ const schema = getTransactionSchema(schemaId, ["nd"]);
1387
+ const nd = schema.properties?.propertyPack?.properties?.notices
1388
+ ?.properties?.neighbourDevelopment;
1389
+ expect(nd?.ntsRef).toBe("N1.1");
1390
+ });
1391
+
1392
+ test("should add ntsRef via nd overlay", () => {
1393
+ const schema = getTransactionSchema(schemaId, ["nd"]);
1394
+ const notices = schema.properties?.propertyPack?.properties?.notices;
1395
+ expect(notices?.ntsRef).toBe("N1");
1396
+ });
1397
+
1398
+ test("should validate No answer", () => {
1399
+ const validator = getSubschemaValidator(
1400
+ "/propertyPack/notices/neighbourDevelopment",
1401
+ schemaId,
1402
+ ["baspiV5", "nd"]
1403
+ );
1404
+ expect(validator({ yesNo: "No" })).toBe(true);
1405
+ });
1406
+ });
1407
+
1408
+ describe("Other material issue (mi)", () => {
1409
+ test("should mark otherMaterialIssue as required", () => {
1410
+ const schema = getTransactionSchema(schemaId, ["mi"]);
1411
+ const additional = schema.properties?.propertyPack?.properties?.additionalInformation;
1412
+ expect(additional?.required).toContain("otherMaterialIssue");
1413
+ });
1414
+
1415
+ test("should not require otherMaterialIssue without mi overlay", () => {
1416
+ const schema = getTransactionSchema(schemaId, []);
1417
+ const additional = schema.properties?.propertyPack?.properties?.additionalInformation;
1418
+ expect(additional?.required || []).not.toContain("otherMaterialIssue");
1419
+ });
1420
+
1421
+ test("should validate No answer", () => {
1422
+ const validator = getSubschemaValidator(
1423
+ "/propertyPack/additionalInformation/otherMaterialIssue",
1424
+ schemaId,
1425
+ ["baspiV5", "mi"]
1426
+ );
1427
+ expect(validator({ yesNo: "No" })).toBe(true);
1428
+ });
1429
+
1430
+ test("should validate Yes with details", () => {
1431
+ const validator = getSubschemaValidator(
1432
+ "/propertyPack/additionalInformation/otherMaterialIssue",
1433
+ schemaId,
1434
+ ["baspiV5", "mi"]
1435
+ );
1436
+ expect(validator({ yesNo: "Yes", details: "Previous subsidence issues" })).toBe(true);
1437
+ });
1438
+ });
1439
+
1440
+ describe("Title restrictions (tr)", () => {
1441
+ test("should require titleRestrictions in freehold branch with tr overlay", () => {
1442
+ const schema = getTransactionSchema(schemaId, ["tr"]);
1443
+ const items = schema.properties?.propertyPack?.properties?.ownership
1444
+ ?.properties?.ownershipsToBeTransferred?.items;
1445
+ // oneOf[0] is Freehold
1446
+ const freeholdBranch = items.oneOf[0];
1447
+ expect(freeholdBranch?.properties?.titleRestrictions).toBeDefined();
1448
+ expect(freeholdBranch?.required).toContain("titleRestrictions");
1449
+ });
1450
+
1451
+ test("should not require titleRestrictions without tr overlay", () => {
1452
+ const schema = getTransactionSchema(schemaId, ["baspiV5"]);
1453
+ const items = schema.properties?.propertyPack?.properties?.ownership
1454
+ ?.properties?.ownershipsToBeTransferred?.items;
1455
+ const freeholdBranch = items.oneOf[0];
1456
+ expect(freeholdBranch?.required || []).not.toContain("titleRestrictions");
1457
+ });
1458
+
1459
+ test("should have correct ntsRef", () => {
1460
+ const schema = getTransactionSchema(schemaId, ["tr"]);
1461
+ const items = schema.properties?.propertyPack?.properties?.ownership
1462
+ ?.properties?.ownershipsToBeTransferred?.items;
1463
+ const freeholdBranch = items.oneOf[0];
1464
+ expect(freeholdBranch?.properties?.titleRestrictions?.ntsRef).toBe("T1.1");
1465
+ });
1466
+ });
1467
+
1468
+ describe("Alterations and changes (ac)", () => {
1469
+ const alterationFields = [
1470
+ "extension",
1471
+ "loftConversion",
1472
+ "garageConversion",
1473
+ "removalOfInternalWalls",
1474
+ "changeOfUse",
1475
+ ];
1476
+
1477
+ test("should expose only selected alterations and changes fields", () => {
1478
+ const schema = getTransactionSchema(schemaId, ["ac"]);
1479
+ const alterations =
1480
+ schema.properties?.propertyPack?.properties?.alterationsAndChanges;
1481
+ const acOverlayAlterations =
1482
+ extensionOverlays.ac.properties?.propertyPack?.properties
1483
+ ?.alterationsAndChanges;
1484
+
1485
+ expect(alterations?.ntsRef).toBe("A1");
1486
+ expect(alterations?.required).toEqual(
1487
+ expect.arrayContaining(alterationFields)
1488
+ );
1489
+ expect(Object.keys(acOverlayAlterations?.properties || {}).sort()).toEqual(
1490
+ alterationFields.slice().sort()
1491
+ );
1492
+
1493
+ alterationFields.forEach((field) => {
1494
+ const fieldSchema = alterations.properties?.[field];
1495
+ const yesBranch = fieldSchema?.oneOf?.[1];
1496
+ const planningPermission =
1497
+ yesBranch?.properties?.planningPermission;
1498
+ const buildingRegApproval =
1499
+ yesBranch?.properties?.buildingRegApproval;
1500
+ const overlayFieldSchema = acOverlayAlterations.properties?.[field];
1501
+ const overlayYesBranch =
1502
+ overlayFieldSchema?.oneOf?.[1];
1503
+ const overlayPlanningPermission =
1504
+ overlayYesBranch?.properties?.planningPermission;
1505
+ const overlayBuildingRegApproval =
1506
+ overlayYesBranch?.properties?.buildingRegApproval;
1507
+
1508
+ expect(fieldSchema?.required).toContain("yesNo");
1509
+ expect(fieldSchema?.discriminator).toEqual({
1510
+ propertyName: "yesNo",
1511
+ });
1512
+ expect(overlayFieldSchema?.discriminator).toEqual({
1513
+ propertyName: "yesNo",
1514
+ });
1515
+ expect(fieldSchema?.properties?.yesNo?.ntsRef).toMatch(/^A1\./);
1516
+ expect(yesBranch?.required).toEqual(
1517
+ expect.arrayContaining([
1518
+ "details",
1519
+ "planningPermission",
1520
+ "buildingRegApproval",
1521
+ ])
1522
+ );
1523
+ expect(Object.keys(overlayYesBranch?.properties || {}).sort()).toEqual([
1524
+ "buildingRegApproval",
1525
+ "details",
1526
+ "planningPermission",
1527
+ ]);
1528
+ expect(planningPermission?.discriminator).toEqual({
1529
+ propertyName: "yesNo",
1530
+ });
1531
+ expect(buildingRegApproval?.discriminator).toEqual({
1532
+ propertyName: "yesNo",
1533
+ });
1534
+ expect(overlayPlanningPermission?.discriminator).toEqual({
1535
+ propertyName: "yesNo",
1536
+ });
1537
+ expect(overlayBuildingRegApproval?.discriminator).toEqual({
1538
+ propertyName: "yesNo",
1539
+ });
1540
+ expect(yesBranch?.properties?.workCompleted?.ntsRef).toBeUndefined();
1541
+ expect(yesBranch?.properties?.documents?.ntsRef).toBeUndefined();
1542
+ expect(yesBranch?.properties?.yearCompleted?.ntsRef).toBeUndefined();
1543
+ expect(yesBranch?.properties?.listedBuildingConsent?.ntsRef).toBeUndefined();
1544
+ expect(yesBranch?.properties?.deedRestrictionConsent?.ntsRef).toBeUndefined();
1545
+ });
1546
+ });
1547
+
1548
+ test("should require details and consent yesNo fields for Yes answers", () => {
1549
+ alterationFields.forEach((field) => {
1550
+ const validator = getSubschemaValidator(
1551
+ `/propertyPack/alterationsAndChanges/${field}`,
1552
+ schemaId,
1553
+ ["ac"]
1554
+ );
1555
+
1556
+ expect(validator({ yesNo: "No" })).toBe(true);
1557
+ expect(validator({ yesNo: "Yes" })).toBe(false);
1558
+ expect(
1559
+ validator({
1560
+ yesNo: "Yes",
1561
+ details: `${field} details`,
1562
+ planningPermission: { yesNo: "Not required" },
1563
+ buildingRegApproval: { yesNo: "Yes" },
1564
+ })
1565
+ ).toBe(true);
1566
+ });
1567
+ });
1568
+ });
1569
+
1570
+ describe("All SEF25 extensions combined", () => {
1571
+ const allSef25 = ["sc", "pc", "ph", "dk", "rw", "sd", "lc", "wg", "ic", "nd", "mi", "tr", "ac"];
1572
+
1573
+ test("should load all SEF25 extensions together without error", () => {
1574
+ const schema = getTransactionSchema(schemaId, ["baspiV5", ...allSef25]);
1575
+ expect(schema).toBeDefined();
1576
+ expect(schema.properties?.propertyPack).toBeDefined();
1577
+ });
1578
+
1579
+ test("should not interfere with existing NTS2 extensions when combined", () => {
1580
+ const schema = getTransactionSchema(schemaId, ["nts2025", ...allSef25]);
1581
+
1582
+ // NTS2 fields still work
1583
+ const si = schema.properties?.propertyPack?.properties?.specialistIssues;
1584
+ expect(si?.properties?.wellsDitchesShaft?.ntsRef).toBe("H1.1");
1585
+
1586
+ // New SEF25 fields present
1587
+ const parking = schema.properties?.propertyPack?.properties?.parking?.properties;
1588
+ expect(parking?.droppedKerbAccess).toBeDefined();
1589
+
1590
+ const env = schema.properties?.propertyPack?.properties?.environmentalIssues?.properties;
1591
+ expect(env?.stormFireFloodDamage).toBeDefined();
1592
+
1593
+ const rights = schema.properties?.propertyPack?.properties?.rightsAndInformalArrangements
1594
+ ?.properties?.rightsOrArrangements?.properties;
1595
+ expect(rights?.privateRightOfWay).toBeDefined();
1596
+ });
1597
+ });
1598
+ });
1599
+ });