@pdtf/schemas 3.6.0-2 → 3.6.0-21

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 (35) hide show
  1. package/.claude/settings.local.json +17 -2
  2. package/docs/sef25-extensions-ui-spec.md +146 -0
  3. package/index.js +97 -58
  4. package/package.json +4 -2
  5. package/src/schemas/v3/combined.json +7013 -1313
  6. package/src/schemas/v3/compactSkeleton.txt +353 -11
  7. package/src/schemas/v3/overlays/baspi4.json +161 -17
  8. package/src/schemas/v3/overlays/baspi5.json +166 -19
  9. package/src/schemas/v3/overlays/extensions/jk.json +24 -6
  10. package/src/schemas/v3/overlays/extensions/pc.json +36 -0
  11. package/src/schemas/v3/overlays/extensions/ph.json +197 -0
  12. package/src/schemas/v3/overlays/extensions/sc.json +83 -0
  13. package/src/schemas/v3/overlays/fme1.json +16 -14
  14. package/src/schemas/v3/overlays/lpe1.json +60 -30
  15. package/src/schemas/v3/overlays/nts.json +68 -1
  16. package/src/schemas/v3/overlays/nts2.json +68 -1
  17. package/src/schemas/v3/overlays/ntsl.json +68 -1
  18. package/src/schemas/v3/overlays/ntsl2.json +68 -1
  19. package/src/schemas/v3/overlays/piq.json +38 -1
  20. package/src/schemas/v3/overlays/rds.json +118 -6
  21. package/src/schemas/v3/overlays/ta6.json +40 -14
  22. package/src/schemas/v3/overlays/ta6ed6.json +6436 -0
  23. package/src/schemas/v3/overlays/ta7.json +70 -6
  24. package/src/schemas/v3/overlays/ta7ed5.json +1539 -0
  25. package/src/schemas/v3/pdtf-transaction.json +4098 -615
  26. package/src/schemas/v3/skeleton.json +491 -34
  27. package/src/tests/v3/buyerCircumstances.test.js +543 -0
  28. package/src/tests/v3/extensionOverlays.test.js +436 -0
  29. package/src/tests/v3/offers.test.js +37 -40
  30. package/src/tests/v3/patternProperties.test.js +149 -30
  31. package/src/tests/v3/ta6ed6.test.js +2356 -0
  32. package/src/tests/v3/ta7ed5.test.js +567 -0
  33. package/src/utils/extractExtensionOverlays.js +104 -31
  34. package/src/utils/extractOverlay.js +87 -23
  35. package/src/utils/pathSkeleton.js +1 -1
@@ -2,6 +2,7 @@ const {
2
2
  getTransactionSchema,
3
3
  extensionOverlays,
4
4
  getValidator,
5
+ getSubschemaValidator,
5
6
  } = require("../../../index.js");
6
7
 
7
8
  const schemaId =
@@ -161,6 +162,9 @@ describe("Extension Overlays", () => {
161
162
  "hi",
162
163
  "fd", // Utilities & Services
163
164
  "oc", // Transaction
165
+ "sc",
166
+ "pc",
167
+ "ph", // SEF25
164
168
  ];
165
169
 
166
170
  expect(Object.keys(extensionOverlays).sort()).toEqual(
@@ -469,6 +473,98 @@ describe("Extension Overlays", () => {
469
473
  ).toBeTruthy();
470
474
  });
471
475
 
476
+ test("should merge supply costs extension into mainsWater No branch", () => {
477
+ const schema = getTransactionSchema(schemaId, ["sc"]);
478
+ const mainsWater =
479
+ schema.properties?.propertyPack?.properties?.waterAndDrainage
480
+ ?.properties?.water?.properties?.mainsWater;
481
+
482
+ expect(mainsWater?.oneOf).toBeDefined();
483
+
484
+ // Branch 0 is yesNo="No" — should now contain associatedCost
485
+ const noBranch = mainsWater.oneOf[0];
486
+ const cost = noBranch?.properties?.associatedCost;
487
+ 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");
492
+ });
493
+
494
+ test("should merge supply costs extension into mainsFoulDrainage No/Not known branch", () => {
495
+ const schema = getTransactionSchema(schemaId, ["sc"]);
496
+ const mainsFoul =
497
+ schema.properties?.propertyPack?.properties?.waterAndDrainage
498
+ ?.properties?.drainage?.properties?.mainsFoulDrainage;
499
+
500
+ expect(mainsFoul?.oneOf).toBeDefined();
501
+
502
+ // Branch 2 is yesNo="No"/"Not known" — should now contain associatedCost
503
+ const noBranch = mainsFoul.oneOf[2];
504
+ const cost = noBranch?.properties?.associatedCost;
505
+ expect(cost).toBeDefined();
506
+ expect(cost.sef25Ref).toBe("U1.2");
507
+ expect(cost.required).toEqual(["amount", "frequency"]);
508
+ });
509
+
510
+ test("should merge parking permit cost and frequency into controlledParking Yes branch", () => {
511
+ const schema = getTransactionSchema(schemaId, ["pc"]);
512
+ const controlled =
513
+ schema.properties?.propertyPack?.properties?.parking?.properties
514
+ ?.controlledParking;
515
+
516
+ expect(controlled?.oneOf).toBeDefined();
517
+
518
+ // Branch 1 is yesNo="Yes"
519
+ const yesBranch = controlled.oneOf[1];
520
+ const cost = yesBranch?.properties?.costOfPermit;
521
+ expect(cost).toBeDefined();
522
+ expect(cost.sef25Ref).toBe("P1.1");
523
+ expect(cost.type).toBe("number");
524
+
525
+ const freq = yesBranch?.properties?.costOfPermitFrequency;
526
+ expect(freq).toBeDefined();
527
+ expect(freq.sef25Ref).toBe("P1.2");
528
+ expect(freq.type).toBe("string");
529
+ });
530
+
531
+ test("should merge all 4 property hazard fields into specialistIssues", () => {
532
+ const schema = getTransactionSchema(schemaId, ["ph"]);
533
+ const si =
534
+ schema.properties?.propertyPack?.properties?.specialistIssues
535
+ ?.properties;
536
+
537
+ const expectedFields = [
538
+ { key: "wellsDitchesShaft", ref: "H1.1" },
539
+ { key: "damagedOrExposedElectrics", ref: "H1.2" },
540
+ { key: "damageToFlooringOrStaircases", ref: "H1.3" },
541
+ { key: "knownAreasInPoorCondition", ref: "H1.4" },
542
+ ];
543
+
544
+ expectedFields.forEach(({ key, ref }) => {
545
+ expect(si[key]).toBeDefined();
546
+ expect(si[key].sef25Ref).toBe(ref);
547
+ expect(si[key].required).toContain("yesNo");
548
+ expect(si[key].discriminator?.propertyName).toBe("yesNo");
549
+ expect(si[key].oneOf).toHaveLength(2);
550
+
551
+ // "No" branch should not require details
552
+ const noBranch = si[key].oneOf.find((b) =>
553
+ b.properties?.yesNo?.enum?.includes("No")
554
+ );
555
+ expect(noBranch).toBeDefined();
556
+ expect(noBranch.required).toBeUndefined();
557
+
558
+ // "Yes" branch should require details
559
+ const yesBranch = si[key].oneOf.find((b) =>
560
+ b.properties?.yesNo?.enum?.includes("Yes")
561
+ );
562
+ expect(yesBranch).toBeDefined();
563
+ expect(yesBranch.required).toContain("details");
564
+ expect(yesBranch.properties.details.minLength).toBe(1);
565
+ });
566
+ });
567
+
472
568
  test("should not require additional fields when 'No' is selected in extension overlays", () => {
473
569
  // Test the 'as' (additional searches) extension overlay
474
570
  const schema = getTransactionSchema(schemaId, ["as"]);
@@ -522,4 +618,344 @@ describe("Extension Overlays", () => {
522
618
  }
523
619
  });
524
620
  });
621
+
622
+ describe("SEF25 extension validation", () => {
623
+ describe("Supply costs (sc)", () => {
624
+ const overlays = ["baspiV5", "sc"];
625
+
626
+ test("should validate valid private water cost", () => {
627
+ const validator = getSubschemaValidator(
628
+ "/propertyPack/waterAndDrainage/water/mainsWater",
629
+ schemaId,
630
+ overlays
631
+ );
632
+
633
+ const result = validator({
634
+ yesNo: "No",
635
+ details: "Private borehole",
636
+ associatedCost: {
637
+ amount: 50,
638
+ frequency: "Per month",
639
+ },
640
+ });
641
+
642
+ expect(result).toBe(true);
643
+ });
644
+
645
+ test("should require associated cost for private water supply", () => {
646
+ const validator = getSubschemaValidator(
647
+ "/propertyPack/waterAndDrainage/water/mainsWater",
648
+ schemaId,
649
+ overlays
650
+ );
651
+
652
+ const result = validator({
653
+ yesNo: "No",
654
+ details: "Private well",
655
+ });
656
+
657
+ // associatedCost is required by the sc extension
658
+ expect(result).toBe(false);
659
+ });
660
+
661
+ test("should reject invalid frequency value for water cost", () => {
662
+ const validator = getSubschemaValidator(
663
+ "/propertyPack/waterAndDrainage/water/mainsWater",
664
+ schemaId,
665
+ overlays
666
+ );
667
+
668
+ const result = validator({
669
+ yesNo: "No",
670
+ details: "Borehole",
671
+ associatedCost: {
672
+ amount: 100,
673
+ frequency: "Weekly",
674
+ },
675
+ });
676
+
677
+ expect(result).toBe(false);
678
+ });
679
+
680
+ test("should reject non-numeric amount for water cost", () => {
681
+ const validator = getSubschemaValidator(
682
+ "/propertyPack/waterAndDrainage/water/mainsWater",
683
+ schemaId,
684
+ overlays
685
+ );
686
+
687
+ const result = validator({
688
+ yesNo: "No",
689
+ details: "Borehole",
690
+ associatedCost: {
691
+ amount: "fifty pounds",
692
+ frequency: "Per month",
693
+ },
694
+ });
695
+
696
+ expect(result).toBe(false);
697
+ });
698
+
699
+ test("should validate valid private sewerage cost", () => {
700
+ // Use sc without baspiV5 to avoid strict offMainsDrainageSystem requirements
701
+ const validator = getSubschemaValidator(
702
+ "/propertyPack/waterAndDrainage/drainage/mainsFoulDrainage",
703
+ schemaId,
704
+ ["sc"]
705
+ );
706
+
707
+ const result = validator({
708
+ yesNo: "No",
709
+ associatedCost: {
710
+ amount: 200,
711
+ frequency: "Per year",
712
+ },
713
+ });
714
+
715
+ expect(result).toBe(true);
716
+ });
717
+
718
+ test("should reject invalid frequency for sewerage cost", () => {
719
+ const validator = getSubschemaValidator(
720
+ "/propertyPack/waterAndDrainage/drainage/mainsFoulDrainage",
721
+ schemaId,
722
+ ["sc"]
723
+ );
724
+
725
+ const result = validator({
726
+ yesNo: "No",
727
+ associatedCost: {
728
+ amount: 200,
729
+ frequency: "Quarterly",
730
+ },
731
+ });
732
+
733
+ expect(result).toBe(false);
734
+ });
735
+
736
+ test("should not allow associated cost when mains water is Yes", () => {
737
+ const validator = getSubschemaValidator(
738
+ "/propertyPack/waterAndDrainage/water/mainsWater",
739
+ schemaId,
740
+ overlays
741
+ );
742
+
743
+ // When yesNo="Yes", associatedCost should cause validation failure
744
+ // because it's only defined in the "No" oneOf branch
745
+ const result = validator({
746
+ yesNo: "Yes",
747
+ supplier: "Thames Water",
748
+ associatedCost: {
749
+ amount: 50,
750
+ frequency: "Per month",
751
+ },
752
+ });
753
+
754
+ expect(result).toBe(false);
755
+ });
756
+ });
757
+
758
+ describe("Parking permit cost (pc)", () => {
759
+ const overlays = ["baspiV5", "pc"];
760
+
761
+ test("should validate valid parking permit with frequency", () => {
762
+ const validator = getSubschemaValidator(
763
+ "/propertyPack/parking/controlledParking",
764
+ schemaId,
765
+ overlays
766
+ );
767
+
768
+ const result = validator({
769
+ yesNo: "Yes",
770
+ costOfPermit: 150,
771
+ costOfPermitFrequency: "Per year",
772
+ });
773
+
774
+ expect(result).toBe(true);
775
+ });
776
+
777
+ test("should validate parking permit with monthly frequency", () => {
778
+ const validator = getSubschemaValidator(
779
+ "/propertyPack/parking/controlledParking",
780
+ schemaId,
781
+ overlays
782
+ );
783
+
784
+ const result = validator({
785
+ yesNo: "Yes",
786
+ costOfPermit: 12.50,
787
+ costOfPermitFrequency: "Per month",
788
+ });
789
+
790
+ expect(result).toBe(true);
791
+ });
792
+
793
+ test("should reject invalid frequency for parking permit", () => {
794
+ const validator = getSubschemaValidator(
795
+ "/propertyPack/parking/controlledParking",
796
+ schemaId,
797
+ overlays
798
+ );
799
+
800
+ const result = validator({
801
+ yesNo: "Yes",
802
+ costOfPermit: 150,
803
+ costOfPermitFrequency: "Per quarter",
804
+ });
805
+
806
+ expect(result).toBe(false);
807
+ });
808
+
809
+ test("should validate No answer without cost fields", () => {
810
+ const validator = getSubschemaValidator(
811
+ "/propertyPack/parking/controlledParking",
812
+ schemaId,
813
+ overlays
814
+ );
815
+
816
+ const result = validator({
817
+ yesNo: "No",
818
+ });
819
+
820
+ expect(result).toBe(true);
821
+ });
822
+ });
823
+
824
+ describe("Property hazards (ph)", () => {
825
+ const overlays = ["ph"];
826
+ const hazardFields = [
827
+ "wellsDitchesShaft",
828
+ "damagedOrExposedElectrics",
829
+ "damageToFlooringOrStaircases",
830
+ "knownAreasInPoorCondition",
831
+ ];
832
+
833
+ hazardFields.forEach((field) => {
834
+ test(`${field}: should validate "No" answer`, () => {
835
+ const validator = getSubschemaValidator(
836
+ `/propertyPack/specialistIssues/${field}`,
837
+ schemaId,
838
+ overlays
839
+ );
840
+
841
+ const result = validator({ yesNo: "No" });
842
+ expect(result).toBe(true);
843
+ });
844
+
845
+ test(`${field}: should validate "Yes" with details`, () => {
846
+ const validator = getSubschemaValidator(
847
+ `/propertyPack/specialistIssues/${field}`,
848
+ schemaId,
849
+ overlays
850
+ );
851
+
852
+ const result = validator({
853
+ yesNo: "Yes",
854
+ details: "Some description of the issue",
855
+ });
856
+ expect(result).toBe(true);
857
+ });
858
+
859
+ test(`${field}: should reject "Yes" without details`, () => {
860
+ const validator = getSubschemaValidator(
861
+ `/propertyPack/specialistIssues/${field}`,
862
+ schemaId,
863
+ overlays
864
+ );
865
+
866
+ const result = validator({ yesNo: "Yes" });
867
+ expect(result).toBe(false);
868
+ });
869
+
870
+ test(`${field}: should reject "Yes" with empty details`, () => {
871
+ const validator = getSubschemaValidator(
872
+ `/propertyPack/specialistIssues/${field}`,
873
+ schemaId,
874
+ overlays
875
+ );
876
+
877
+ const result = validator({ yesNo: "Yes", details: "" });
878
+ expect(result).toBe(false);
879
+ });
880
+
881
+ test(`${field}: should reject invalid yesNo value`, () => {
882
+ const validator = getSubschemaValidator(
883
+ `/propertyPack/specialistIssues/${field}`,
884
+ schemaId,
885
+ overlays
886
+ );
887
+
888
+ const result = validator({ yesNo: "Maybe" });
889
+ expect(result).toBe(false);
890
+ });
891
+ });
892
+ });
893
+
894
+ describe("Combined SEF25 extensions", () => {
895
+ test("should load all three SEF25 extensions together", () => {
896
+ const schema = getTransactionSchema(schemaId, [
897
+ "baspiV5",
898
+ "sc",
899
+ "pc",
900
+ "ph",
901
+ ]);
902
+
903
+ // Supply costs
904
+ const waterCost =
905
+ schema.properties?.propertyPack?.properties?.waterAndDrainage
906
+ ?.properties?.water?.properties?.mainsWater?.oneOf?.[0]?.properties
907
+ ?.associatedCost;
908
+ expect(waterCost).toBeDefined();
909
+
910
+ const sewerageCost =
911
+ schema.properties?.propertyPack?.properties?.waterAndDrainage
912
+ ?.properties?.drainage?.properties?.mainsFoulDrainage?.oneOf?.[2]
913
+ ?.properties?.associatedCost;
914
+ expect(sewerageCost).toBeDefined();
915
+
916
+ // Parking
917
+ const parkingFreq =
918
+ schema.properties?.propertyPack?.properties?.parking?.properties
919
+ ?.controlledParking?.oneOf?.[1]?.properties?.costOfPermitFrequency;
920
+ expect(parkingFreq).toBeDefined();
921
+
922
+ // Hazards
923
+ const si =
924
+ schema.properties?.propertyPack?.properties?.specialistIssues
925
+ ?.properties;
926
+ expect(si?.wellsDitchesShaft).toBeDefined();
927
+ expect(si?.damagedOrExposedElectrics).toBeDefined();
928
+ expect(si?.damageToFlooringOrStaircases).toBeDefined();
929
+ expect(si?.knownAreasInPoorCondition).toBeDefined();
930
+ });
931
+
932
+ test("should not interfere with existing NTS2 extensions", () => {
933
+ const schema = getTransactionSchema(schemaId, [
934
+ "nts2023",
935
+ "jk",
936
+ "hs",
937
+ "sc",
938
+ "pc",
939
+ "ph",
940
+ ]);
941
+
942
+ const si =
943
+ schema.properties?.propertyPack?.properties?.specialistIssues;
944
+
945
+ // Existing NTS2 extensions still work
946
+ expect(si?.properties?.japaneseKnotweed?.ntsRef).toBe("A5.3");
947
+ expect(si?.properties?.ongoingHealthOrSafetyIssue?.ntsRef).toBe(
948
+ "A5.5"
949
+ );
950
+ expect(si?.required).toContain("japaneseKnotweed");
951
+ expect(si?.required).toContain("ongoingHealthOrSafetyIssue");
952
+
953
+ // SEF25 hazards also present
954
+ expect(si?.properties?.wellsDitchesShaft?.sef25Ref).toBe("H1.1");
955
+ expect(si?.properties?.knownAreasInPoorCondition?.sef25Ref).toBe(
956
+ "H1.4"
957
+ );
958
+ });
959
+ });
960
+ });
525
961
  });