@pdtf/schemas 3.6.0-18 → 3.6.0-19

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.
@@ -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,96 @@ 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 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 freq = yesBranch?.properties?.costOfPermitFrequency;
521
+ expect(freq).toBeDefined();
522
+ expect(freq.sef25Ref).toBe("P1.1");
523
+ expect(freq.type).toBe("string");
524
+
525
+ // Existing annualCostOfPermit should still be present
526
+ expect(yesBranch.properties.annualCostOfPermit).toBeDefined();
527
+ });
528
+
529
+ test("should merge all 4 property hazard fields into specialistIssues", () => {
530
+ const schema = getTransactionSchema(schemaId, ["ph"]);
531
+ const si =
532
+ schema.properties?.propertyPack?.properties?.specialistIssues
533
+ ?.properties;
534
+
535
+ const expectedFields = [
536
+ { key: "wellsDitchesShaft", ref: "H1.1" },
537
+ { key: "damagedOrExposedElectrics", ref: "H1.2" },
538
+ { key: "damageToFlooringOrStaircases", ref: "H1.3" },
539
+ { key: "knownAreasInPoorCondition", ref: "H1.4" },
540
+ ];
541
+
542
+ expectedFields.forEach(({ key, ref }) => {
543
+ expect(si[key]).toBeDefined();
544
+ expect(si[key].sef25Ref).toBe(ref);
545
+ expect(si[key].required).toContain("yesNo");
546
+ expect(si[key].discriminator?.propertyName).toBe("yesNo");
547
+ expect(si[key].oneOf).toHaveLength(2);
548
+
549
+ // "No" branch should not require details
550
+ const noBranch = si[key].oneOf.find((b) =>
551
+ b.properties?.yesNo?.enum?.includes("No")
552
+ );
553
+ expect(noBranch).toBeDefined();
554
+ expect(noBranch.required).toBeUndefined();
555
+
556
+ // "Yes" branch should require details
557
+ const yesBranch = si[key].oneOf.find((b) =>
558
+ b.properties?.yesNo?.enum?.includes("Yes")
559
+ );
560
+ expect(yesBranch).toBeDefined();
561
+ expect(yesBranch.required).toContain("details");
562
+ expect(yesBranch.properties.details.minLength).toBe(1);
563
+ });
564
+ });
565
+
472
566
  test("should not require additional fields when 'No' is selected in extension overlays", () => {
473
567
  // Test the 'as' (additional searches) extension overlay
474
568
  const schema = getTransactionSchema(schemaId, ["as"]);
@@ -522,4 +616,343 @@ describe("Extension Overlays", () => {
522
616
  }
523
617
  });
524
618
  });
619
+
620
+ describe("SEF25 extension validation", () => {
621
+ describe("Supply costs (sc)", () => {
622
+ const overlays = ["baspiV5", "sc"];
623
+
624
+ test("should validate valid private water cost", () => {
625
+ const validator = getSubschemaValidator(
626
+ "/propertyPack/waterAndDrainage/water/mainsWater",
627
+ schemaId,
628
+ overlays
629
+ );
630
+
631
+ const result = validator({
632
+ yesNo: "No",
633
+ details: "Private borehole",
634
+ associatedCost: {
635
+ amount: 50,
636
+ frequency: "Per month",
637
+ },
638
+ });
639
+
640
+ expect(result).toBe(true);
641
+ });
642
+
643
+ test("should validate private water with no associated cost (cost is optional at schema level)", () => {
644
+ const validator = getSubschemaValidator(
645
+ "/propertyPack/waterAndDrainage/water/mainsWater",
646
+ schemaId,
647
+ overlays
648
+ );
649
+
650
+ const result = validator({
651
+ yesNo: "No",
652
+ details: "Private well",
653
+ });
654
+
655
+ expect(result).toBe(true);
656
+ });
657
+
658
+ test("should reject invalid frequency value for water cost", () => {
659
+ const validator = getSubschemaValidator(
660
+ "/propertyPack/waterAndDrainage/water/mainsWater",
661
+ schemaId,
662
+ overlays
663
+ );
664
+
665
+ const result = validator({
666
+ yesNo: "No",
667
+ details: "Borehole",
668
+ associatedCost: {
669
+ amount: 100,
670
+ frequency: "Weekly",
671
+ },
672
+ });
673
+
674
+ expect(result).toBe(false);
675
+ });
676
+
677
+ test("should reject non-numeric amount for water cost", () => {
678
+ const validator = getSubschemaValidator(
679
+ "/propertyPack/waterAndDrainage/water/mainsWater",
680
+ schemaId,
681
+ overlays
682
+ );
683
+
684
+ const result = validator({
685
+ yesNo: "No",
686
+ details: "Borehole",
687
+ associatedCost: {
688
+ amount: "fifty pounds",
689
+ frequency: "Per month",
690
+ },
691
+ });
692
+
693
+ expect(result).toBe(false);
694
+ });
695
+
696
+ test("should validate valid private sewerage cost", () => {
697
+ // Use sc without baspiV5 to avoid strict offMainsDrainageSystem requirements
698
+ const validator = getSubschemaValidator(
699
+ "/propertyPack/waterAndDrainage/drainage/mainsFoulDrainage",
700
+ schemaId,
701
+ ["sc"]
702
+ );
703
+
704
+ const result = validator({
705
+ yesNo: "No",
706
+ associatedCost: {
707
+ amount: 200,
708
+ frequency: "Per year",
709
+ },
710
+ });
711
+
712
+ expect(result).toBe(true);
713
+ });
714
+
715
+ test("should reject invalid frequency for sewerage cost", () => {
716
+ const validator = getSubschemaValidator(
717
+ "/propertyPack/waterAndDrainage/drainage/mainsFoulDrainage",
718
+ schemaId,
719
+ ["sc"]
720
+ );
721
+
722
+ const result = validator({
723
+ yesNo: "No",
724
+ associatedCost: {
725
+ amount: 200,
726
+ frequency: "Quarterly",
727
+ },
728
+ });
729
+
730
+ expect(result).toBe(false);
731
+ });
732
+
733
+ test("should not allow associated cost when mains water is Yes", () => {
734
+ const validator = getSubschemaValidator(
735
+ "/propertyPack/waterAndDrainage/water/mainsWater",
736
+ schemaId,
737
+ overlays
738
+ );
739
+
740
+ // When yesNo="Yes", associatedCost should cause validation failure
741
+ // because it's only defined in the "No" oneOf branch
742
+ const result = validator({
743
+ yesNo: "Yes",
744
+ supplier: "Thames Water",
745
+ associatedCost: {
746
+ amount: 50,
747
+ frequency: "Per month",
748
+ },
749
+ });
750
+
751
+ expect(result).toBe(false);
752
+ });
753
+ });
754
+
755
+ describe("Parking permit cost (pc)", () => {
756
+ const overlays = ["baspiV5", "pc"];
757
+
758
+ test("should validate valid parking permit with frequency", () => {
759
+ const validator = getSubschemaValidator(
760
+ "/propertyPack/parking/controlledParking",
761
+ schemaId,
762
+ overlays
763
+ );
764
+
765
+ const result = validator({
766
+ yesNo: "Yes",
767
+ annualCostOfPermit: 150,
768
+ costOfPermitFrequency: "Per year",
769
+ });
770
+
771
+ expect(result).toBe(true);
772
+ });
773
+
774
+ test("should validate parking permit with monthly frequency", () => {
775
+ const validator = getSubschemaValidator(
776
+ "/propertyPack/parking/controlledParking",
777
+ schemaId,
778
+ overlays
779
+ );
780
+
781
+ const result = validator({
782
+ yesNo: "Yes",
783
+ annualCostOfPermit: 12.50,
784
+ costOfPermitFrequency: "Per month",
785
+ });
786
+
787
+ expect(result).toBe(true);
788
+ });
789
+
790
+ test("should reject invalid frequency for parking permit", () => {
791
+ const validator = getSubschemaValidator(
792
+ "/propertyPack/parking/controlledParking",
793
+ schemaId,
794
+ overlays
795
+ );
796
+
797
+ const result = validator({
798
+ yesNo: "Yes",
799
+ annualCostOfPermit: 150,
800
+ costOfPermitFrequency: "Per quarter",
801
+ });
802
+
803
+ expect(result).toBe(false);
804
+ });
805
+
806
+ test("should validate No answer without cost fields", () => {
807
+ const validator = getSubschemaValidator(
808
+ "/propertyPack/parking/controlledParking",
809
+ schemaId,
810
+ overlays
811
+ );
812
+
813
+ const result = validator({
814
+ yesNo: "No",
815
+ });
816
+
817
+ expect(result).toBe(true);
818
+ });
819
+ });
820
+
821
+ describe("Property hazards (ph)", () => {
822
+ const overlays = ["ph"];
823
+ const hazardFields = [
824
+ "wellsDitchesShaft",
825
+ "damagedOrExposedElectrics",
826
+ "damageToFlooringOrStaircases",
827
+ "knownAreasInPoorCondition",
828
+ ];
829
+
830
+ hazardFields.forEach((field) => {
831
+ test(`${field}: should validate "No" answer`, () => {
832
+ const validator = getSubschemaValidator(
833
+ `/propertyPack/specialistIssues/${field}`,
834
+ schemaId,
835
+ overlays
836
+ );
837
+
838
+ const result = validator({ yesNo: "No" });
839
+ expect(result).toBe(true);
840
+ });
841
+
842
+ test(`${field}: should validate "Yes" with details`, () => {
843
+ const validator = getSubschemaValidator(
844
+ `/propertyPack/specialistIssues/${field}`,
845
+ schemaId,
846
+ overlays
847
+ );
848
+
849
+ const result = validator({
850
+ yesNo: "Yes",
851
+ details: "Some description of the issue",
852
+ });
853
+ expect(result).toBe(true);
854
+ });
855
+
856
+ test(`${field}: should reject "Yes" without details`, () => {
857
+ const validator = getSubschemaValidator(
858
+ `/propertyPack/specialistIssues/${field}`,
859
+ schemaId,
860
+ overlays
861
+ );
862
+
863
+ const result = validator({ yesNo: "Yes" });
864
+ expect(result).toBe(false);
865
+ });
866
+
867
+ test(`${field}: should reject "Yes" with empty details`, () => {
868
+ const validator = getSubschemaValidator(
869
+ `/propertyPack/specialistIssues/${field}`,
870
+ schemaId,
871
+ overlays
872
+ );
873
+
874
+ const result = validator({ yesNo: "Yes", details: "" });
875
+ expect(result).toBe(false);
876
+ });
877
+
878
+ test(`${field}: should reject invalid yesNo value`, () => {
879
+ const validator = getSubschemaValidator(
880
+ `/propertyPack/specialistIssues/${field}`,
881
+ schemaId,
882
+ overlays
883
+ );
884
+
885
+ const result = validator({ yesNo: "Maybe" });
886
+ expect(result).toBe(false);
887
+ });
888
+ });
889
+ });
890
+
891
+ describe("Combined SEF25 extensions", () => {
892
+ test("should load all three SEF25 extensions together", () => {
893
+ const schema = getTransactionSchema(schemaId, [
894
+ "baspiV5",
895
+ "sc",
896
+ "pc",
897
+ "ph",
898
+ ]);
899
+
900
+ // Supply costs
901
+ const waterCost =
902
+ schema.properties?.propertyPack?.properties?.waterAndDrainage
903
+ ?.properties?.water?.properties?.mainsWater?.oneOf?.[0]?.properties
904
+ ?.associatedCost;
905
+ expect(waterCost).toBeDefined();
906
+
907
+ const sewerageCost =
908
+ schema.properties?.propertyPack?.properties?.waterAndDrainage
909
+ ?.properties?.drainage?.properties?.mainsFoulDrainage?.oneOf?.[2]
910
+ ?.properties?.associatedCost;
911
+ expect(sewerageCost).toBeDefined();
912
+
913
+ // Parking
914
+ const parkingFreq =
915
+ schema.properties?.propertyPack?.properties?.parking?.properties
916
+ ?.controlledParking?.oneOf?.[1]?.properties?.costOfPermitFrequency;
917
+ expect(parkingFreq).toBeDefined();
918
+
919
+ // Hazards
920
+ const si =
921
+ schema.properties?.propertyPack?.properties?.specialistIssues
922
+ ?.properties;
923
+ expect(si?.wellsDitchesShaft).toBeDefined();
924
+ expect(si?.damagedOrExposedElectrics).toBeDefined();
925
+ expect(si?.damageToFlooringOrStaircases).toBeDefined();
926
+ expect(si?.knownAreasInPoorCondition).toBeDefined();
927
+ });
928
+
929
+ test("should not interfere with existing NTS2 extensions", () => {
930
+ const schema = getTransactionSchema(schemaId, [
931
+ "nts2023",
932
+ "jk",
933
+ "hs",
934
+ "sc",
935
+ "pc",
936
+ "ph",
937
+ ]);
938
+
939
+ const si =
940
+ schema.properties?.propertyPack?.properties?.specialistIssues;
941
+
942
+ // Existing NTS2 extensions still work
943
+ expect(si?.properties?.japaneseKnotweed?.ntsRef).toBe("A5.3");
944
+ expect(si?.properties?.ongoingHealthOrSafetyIssue?.ntsRef).toBe(
945
+ "A5.5"
946
+ );
947
+ expect(si?.required).toContain("japaneseKnotweed");
948
+ expect(si?.required).toContain("ongoingHealthOrSafetyIssue");
949
+
950
+ // SEF25 hazards also present
951
+ expect(si?.properties?.wellsDitchesShaft?.sef25Ref).toBe("H1.1");
952
+ expect(si?.properties?.knownAreasInPoorCondition?.sef25Ref).toBe(
953
+ "H1.4"
954
+ );
955
+ });
956
+ });
957
+ });
525
958
  });
@@ -5,6 +5,30 @@ const path = require("path");
5
5
 
6
6
  const combinedSchema = require("../schemas/v3/combined.json");
7
7
 
8
+ // Derive ref-related field names from a refType
9
+ function getRefConfig(refType) {
10
+ if (refType === "nts2Ref") {
11
+ // Legacy mapping: nts2Ref -> ntsRef in output
12
+ return {
13
+ ref: "nts2Ref",
14
+ outputRef: "ntsRef",
15
+ required: "nts2Required",
16
+ title: "nts2Title",
17
+ description: "nts2Description",
18
+ enumKey: "nts2Enum",
19
+ };
20
+ }
21
+ // For other ref types, derive keys by replacing "Ref" suffix
22
+ return {
23
+ ref: refType,
24
+ outputRef: refType,
25
+ required: refType.replace("Ref", "Required"),
26
+ title: refType.replace("Ref", "Title"),
27
+ description: refType.replace("Ref", "Description"),
28
+ enumKey: refType.replace("Ref", "Enum"),
29
+ };
30
+ }
31
+
8
32
  // Define the extension overlay mappings
9
33
  const extensionMappings = {
10
34
  // Outside areas
@@ -150,18 +174,52 @@ const extensionMappings = {
150
174
  "/properties/propertyPack/properties/completionAndMoving/properties/otherPropertyInChain",
151
175
  ],
152
176
  },
177
+
178
+ // Private supply costs (water & sewerage)
179
+ sc: {
180
+ name: "supplyCosts",
181
+ description: "Associated costs for private water and sewerage supply",
182
+ refType: "sef25Ref",
183
+ paths: [
184
+ "/properties/propertyPack/properties/waterAndDrainage/properties/water/properties/mainsWater/oneOf/0/properties/associatedCost",
185
+ "/properties/propertyPack/properties/waterAndDrainage/properties/drainage/properties/mainsFoulDrainage/oneOf/2/properties/associatedCost",
186
+ ],
187
+ },
188
+
189
+ // Parking permit costs
190
+ pc: {
191
+ name: "parkingPermitCost",
192
+ description: "Parking permit cost and frequency",
193
+ refType: "sef25Ref",
194
+ paths: [
195
+ "/properties/propertyPack/properties/parking/properties/controlledParking/oneOf/1/properties/costOfPermitFrequency",
196
+ ],
197
+ },
198
+
199
+ // Property hazards
200
+ ph: {
201
+ name: "propertyHazards",
202
+ description: "Property hazards and known issues",
203
+ refType: "sef25Ref",
204
+ paths: [
205
+ "/properties/propertyPack/properties/specialistIssues/properties/wellsDitchesShaft",
206
+ "/properties/propertyPack/properties/specialistIssues/properties/damagedOrExposedElectrics",
207
+ "/properties/propertyPack/properties/specialistIssues/properties/damageToFlooringOrStaircases",
208
+ "/properties/propertyPack/properties/specialistIssues/properties/knownAreasInPoorCondition",
209
+ ],
210
+ },
153
211
  };
154
212
 
155
- // Function to extract properties at specific paths with nts2Ref
156
- function extractPathsWithNts2Ref(schema, paths) {
213
+ // Function to extract properties at specific paths with given ref type
214
+ function extractPathsWithRef(schema, paths, refConfig) {
157
215
  const result = {};
158
216
 
159
217
  paths.forEach((path) => {
160
218
  try {
161
219
  const value = jp.get(schema, path);
162
- if (value && hasNts2Ref(value)) {
220
+ if (value && hasRef(value, refConfig)) {
163
221
  // Set the value at the same path in result
164
- jp.set(result, path, extractNts2Properties(value));
222
+ jp.set(result, path, extractRefProperties(value, refConfig));
165
223
  }
166
224
  } catch (err) {
167
225
  console.warn(`Path not found: ${path}`);
@@ -171,11 +229,11 @@ function extractPathsWithNts2Ref(schema, paths) {
171
229
  return result;
172
230
  }
173
231
 
174
- // Check if an object or its descendants have nts2Ref
175
- function hasNts2Ref(obj) {
232
+ // Check if an object or its descendants have the specified ref
233
+ function hasRef(obj, refConfig) {
176
234
  let found = false;
177
235
  traverse(obj).forEach(function (element) {
178
- if (element && element.nts2Ref) {
236
+ if (element && element[refConfig.ref]) {
179
237
  found = true;
180
238
  this.stop();
181
239
  }
@@ -183,16 +241,16 @@ function hasNts2Ref(obj) {
183
241
  return found;
184
242
  }
185
243
 
186
- // Extract only NTS2-specific properties (with nts2Ref)
187
- function extractNts2Properties(obj, parentKey) {
244
+ // Extract only ref-specific properties
245
+ function extractRefProperties(obj, refConfig, parentKey) {
188
246
  const result = {};
189
247
 
190
- // Copy nts2-specific metadata at current level
191
- if (obj.nts2Ref) result.ntsRef = obj.nts2Ref;
192
- if (obj.nts2Required) result.required = obj.nts2Required;
193
- if (obj.nts2Title) result.title = obj.nts2Title;
194
- if (obj.nts2Description) result.description = obj.nts2Description;
195
- if (obj.nts2Enum) result.enum = obj.nts2Enum;
248
+ // Copy ref-specific metadata at current level
249
+ if (obj[refConfig.ref]) result[refConfig.outputRef] = obj[refConfig.ref];
250
+ if (obj[refConfig.required]) result.required = obj[refConfig.required];
251
+ if (obj[refConfig.title]) result.title = obj[refConfig.title];
252
+ if (obj[refConfig.description]) result.description = obj[refConfig.description];
253
+ if (obj[refConfig.enumKey]) result.enum = obj[refConfig.enumKey];
196
254
 
197
255
  // Handle discriminator
198
256
  if (obj.discriminator) {
@@ -204,11 +262,11 @@ function extractNts2Properties(obj, parentKey) {
204
262
  result.properties = {};
205
263
  Object.keys(obj.properties).forEach((key) => {
206
264
  const prop = obj.properties[key];
207
- if (hasNts2Ref(prop)) {
208
- result.properties[key] = extractNts2Properties(prop, key);
265
+ if (hasRef(prop, refConfig)) {
266
+ result.properties[key] = extractRefProperties(prop, refConfig, key);
209
267
  }
210
268
  });
211
- // Only keep properties if we found some with nts2Ref
269
+ // Only keep properties if we found some with ref
212
270
  if (Object.keys(result.properties).length === 0) {
213
271
  delete result.properties;
214
272
  }
@@ -216,8 +274,8 @@ function extractNts2Properties(obj, parentKey) {
216
274
 
217
275
  // Handle arrays
218
276
  if (obj.items) {
219
- if (hasNts2Ref(obj.items)) {
220
- result.items = extractNts2Properties(obj.items);
277
+ if (hasRef(obj.items, refConfig)) {
278
+ result.items = extractRefProperties(obj.items, refConfig);
221
279
  }
222
280
  }
223
281
 
@@ -225,8 +283,8 @@ function extractNts2Properties(obj, parentKey) {
225
283
  if (obj.oneOf) {
226
284
  // Always preserve the full array structure and order
227
285
  const processedOneOf = obj.oneOf.map((schema, index) => {
228
- if (hasNts2Ref(schema)) {
229
- const extracted = extractNts2Properties(schema);
286
+ if (hasRef(schema, refConfig)) {
287
+ const extracted = extractRefProperties(schema, refConfig);
230
288
  // For oneOf schemas, we need to preserve discriminator enum values
231
289
  if (obj.discriminator && schema.properties) {
232
290
  const propName = obj.discriminator.propertyName;
@@ -234,7 +292,7 @@ function extractNts2Properties(obj, parentKey) {
234
292
  if (!extracted.properties) extracted.properties = {};
235
293
  extracted.properties[propName] = {
236
294
  enum:
237
- schema.properties[propName].nts2Enum ||
295
+ schema.properties[propName][refConfig.enumKey] ||
238
296
  schema.properties[propName].enum,
239
297
  };
240
298
  }
@@ -258,7 +316,7 @@ function extractNts2Properties(obj, parentKey) {
258
316
  }
259
317
  return extracted;
260
318
  } else {
261
- // If this branch doesn't have nts2Ref, preserve the base discriminator enum
319
+ // If this branch doesn't have ref, preserve the base discriminator enum
262
320
  if (obj.discriminator && schema.properties) {
263
321
  const propName = obj.discriminator.propertyName;
264
322
  if (schema.properties[propName]) {
@@ -281,7 +339,7 @@ function extractNts2Properties(obj, parentKey) {
281
339
  // If parentKey is set, and this object has extension metadata, return a stub for the parent
282
340
  if (
283
341
  parentKey &&
284
- (result.ntsRef || result.required || result.discriminator)
342
+ (result[refConfig.outputRef] || result.required || result.discriminator)
285
343
  ) {
286
344
  // This is an extension property inside a oneOf branch
287
345
  // Return a stub for the parent property with metadata and oneOf structure
@@ -311,16 +369,16 @@ function extractNts2Properties(obj, parentKey) {
311
369
  }
312
370
 
313
371
  // Add required array at parent levels if needed
314
- function addRequiredArrays(overlay, schema) {
372
+ function addRequiredArrays(overlay, schema, refConfig) {
315
373
  traverse(overlay).forEach(function (node) {
316
374
  if (node && node.properties) {
317
375
  const schemaPath = "/" + this.path.join("/");
318
376
  try {
319
377
  const schemaNode = jp.get(schema, schemaPath);
320
- if (schemaNode && schemaNode.nts2Required) {
378
+ if (schemaNode && schemaNode[refConfig.required]) {
321
379
  // Filter to only include properties that exist in this overlay
322
380
  const overlayProps = Object.keys(node.properties);
323
- const requiredProps = schemaNode.nts2Required.filter((prop) =>
381
+ const requiredProps = schemaNode[refConfig.required].filter((prop) =>
324
382
  overlayProps.includes(prop)
325
383
  );
326
384
  if (requiredProps.length > 0) {
@@ -337,13 +395,16 @@ function addRequiredArrays(overlay, schema) {
337
395
 
338
396
  // Generate extension overlays
339
397
  Object.entries(extensionMappings).forEach(([code, config]) => {
340
- console.log(`\nGenerating extension overlay: ${code} (${config.name})`);
398
+ const refType = config.refType || "nts2Ref";
399
+ const refConfig = getRefConfig(refType);
400
+
401
+ console.log(`\nGenerating extension overlay: ${code} (${config.name}) [${refType}]`);
341
402
 
342
403
  // Extract the specific paths
343
- let overlay = extractPathsWithNts2Ref(combinedSchema, config.paths);
404
+ let overlay = extractPathsWithRef(combinedSchema, config.paths, refConfig);
344
405
 
345
406
  // Add required arrays where needed
346
- overlay = addRequiredArrays(overlay, combinedSchema);
407
+ overlay = addRequiredArrays(overlay, combinedSchema, refConfig);
347
408
 
348
409
  // Add schema metadata
349
410
  overlay.$schema = "http://json-schema.org/draft-07/schema#";