ti2-tourplan 1.0.129-beta.1 → 1.0.129

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.
package/index.test.js CHANGED
@@ -863,66 +863,287 @@ describe('search tests', () => {
863
863
  expect(retVal).toMatchSnapshot();
864
864
  });
865
865
 
866
- it('searchProductsForItinerary skips AC in full catalog when skipAccommodation is set', async () => {
867
- const debugSpy = jest.spyOn(console, 'debug').mockImplementation(() => {});
868
- const bdOption = {
869
- Opt: 'TESTBDOPTION00001',
870
- OptGeneral: {
871
- SupplierId: '1001',
872
- SupplierName: 'Test Supplier',
873
- Description: 'Test BD Product',
874
- SType: 'N',
875
- AdultsAllowed: 'Y',
876
- Adult_From: 16,
877
- Adult_To: 999,
878
- ButtonName: 'Sightseeing',
866
+ describe('searchProductsForItinerary omitServiceCodes', () => {
867
+ const optionGeneral = {
868
+ SupplierId: '1001',
869
+ SupplierName: 'Test Supplier',
870
+ Description: 'Test Product',
871
+ SType: 'N',
872
+ AdultsAllowed: 'Y',
873
+ Adult_From: 16,
874
+ Adult_To: 999,
875
+ ButtonName: 'Sightseeing',
876
+ };
877
+ const acOption = { Opt: 'TESTACOPTION00001', OptGeneral: optionGeneral };
878
+ const bdOption = { Opt: 'TESTBDOPTION00001', OptGeneral: optionGeneral };
879
+ const trOption = { Opt: 'TESTTROPTION00001', OptGeneral: optionGeneral };
880
+ const getServicesReply = {
881
+ GetServicesReply: {
882
+ TPLServices: {
883
+ TPLService: [
884
+ { Code: 'AC', Name: 'Accommodation' },
885
+ { Code: 'BD', Name: 'Bundle' },
886
+ { Code: 'TR', Name: 'Transfer' },
887
+ ],
888
+ },
879
889
  },
880
890
  };
891
+ const optionsByOpt = {
892
+ '???AC????????????': acOption,
893
+ '???BD????????????': bdOption,
894
+ '???TR????????????': trOption,
895
+ };
896
+ const defaultCallTourplan = mockCallTourplan.getMockImplementation();
897
+ const getOptionInfoCalls = () => mockCallTourplan.mock.calls
898
+ .map(([call]) => call.model.OptionInfoRequest)
899
+ .filter(Boolean)
900
+ .map(req => req.Opt);
881
901
 
882
- mockCallTourplan
883
- .mockImplementationOnce(async () => ({
884
- GetServicesReply: {
885
- TPLServices: {
886
- TPLService: [
887
- { Code: 'AC', Name: 'Accommodation' },
888
- { Code: 'BD', Name: 'Bundle' },
889
- ],
890
- },
891
- },
892
- }))
893
- .mockImplementationOnce(async ({ model }) => {
894
- const opt = model.OptionInfoRequest.Opt;
895
- if (opt === '???AC????????????') {
896
- throw new Error('AC OptionInfo should have been skipped');
902
+ afterEach(() => {
903
+ mockCallTourplan.mockImplementation(defaultCallTourplan);
904
+ });
905
+
906
+ const mockFullCatalog = ({ throwOnOpt } = {}) => {
907
+ mockCallTourplan.mockImplementation(async ({ model }) => {
908
+ if (model.GetServicesRequest) return getServicesReply;
909
+ const opt = model.OptionInfoRequest && model.OptionInfoRequest.Opt;
910
+ if (throwOnOpt && throwOnOpt.includes(opt)) {
911
+ throw new Error(`${opt} OptionInfo should have been skipped`);
897
912
  }
898
- if (opt === '???BD????????????') {
899
- return { OptionInfoReply: { Option: bdOption } };
913
+ if (optionsByOpt[opt]) {
914
+ return { OptionInfoReply: { Option: optionsByOpt[opt] } };
900
915
  }
901
916
  throw new Error(`Unexpected OptionInfo Opt: ${opt}`);
902
917
  });
918
+ };
903
919
 
904
- const retVal = await app.searchProductsForItinerary({
905
- axios,
906
- token,
907
- typeDefsAndQueries,
908
- payload: {
909
- searchInput: '',
910
- skipAccommodation: true,
911
- },
920
+ it('fetches all service codes when omitServiceCodes is not provided', async () => {
921
+ const debugSpy = jest.spyOn(console, 'debug').mockImplementation(() => {});
922
+ mockFullCatalog();
923
+
924
+ const retVal = await app.searchProductsForItinerary({
925
+ axios,
926
+ token,
927
+ typeDefsAndQueries,
928
+ payload: { searchInput: '' },
929
+ });
930
+
931
+ expect(getOptionInfoCalls()).toEqual([
932
+ '???AC????????????',
933
+ '???BD????????????',
934
+ '???TR????????????',
935
+ ]);
936
+ expect(debugSpy).not.toHaveBeenCalled();
937
+ expect(retVal.products).toHaveLength(1);
938
+ expect(retVal.products[0].options.map(o => o.optionId)).toEqual([
939
+ 'TESTACOPTION00001',
940
+ 'TESTBDOPTION00001',
941
+ 'TESTTROPTION00001',
942
+ ]);
943
+ debugSpy.mockRestore();
912
944
  });
913
945
 
914
- const optionInfoCalls = mockCallTourplan.mock.calls
915
- .map(([call]) => call.model.OptionInfoRequest)
916
- .filter(Boolean);
917
- expect(optionInfoCalls).toHaveLength(1);
918
- expect(optionInfoCalls[0].Opt).toBe('???BD????????????');
919
- expect(debugSpy).toHaveBeenCalledWith(
920
- '[tourplan] Accommodation (AC) service code will be skipped in full catalog product search',
921
- );
922
- expect(retVal.products).toHaveLength(1);
923
- expect(retVal.products[0].options[0].optionId).toBe('TESTBDOPTION00001');
946
+ it('fetches all service codes when omitServiceCodes is empty', async () => {
947
+ const debugSpy = jest.spyOn(console, 'debug').mockImplementation(() => {});
948
+ mockFullCatalog();
949
+
950
+ const retVal = await app.searchProductsForItinerary({
951
+ axios,
952
+ token,
953
+ typeDefsAndQueries,
954
+ payload: {
955
+ searchInput: '',
956
+ omitServiceCodes: [],
957
+ },
958
+ });
959
+
960
+ expect(getOptionInfoCalls()).toEqual([
961
+ '???AC????????????',
962
+ '???BD????????????',
963
+ '???TR????????????',
964
+ ]);
965
+ expect(debugSpy).not.toHaveBeenCalled();
966
+ expect(retVal.products[0].options).toHaveLength(3);
967
+ debugSpy.mockRestore();
968
+ });
969
+
970
+ it('omits a single listed service code', async () => {
971
+ const debugSpy = jest.spyOn(console, 'debug').mockImplementation(() => {});
972
+ mockFullCatalog({ throwOnOpt: ['???AC????????????'] });
973
+
974
+ const retVal = await app.searchProductsForItinerary({
975
+ axios,
976
+ token,
977
+ typeDefsAndQueries,
978
+ payload: {
979
+ searchInput: '',
980
+ omitServiceCodes: ['AC'],
981
+ },
982
+ });
983
+
984
+ expect(getOptionInfoCalls()).toEqual([
985
+ '???BD????????????',
986
+ '???TR????????????',
987
+ ]);
988
+ expect(debugSpy).toHaveBeenCalledWith(
989
+ '[tourplan] Omitting service code(s) from full catalog product search: AC',
990
+ );
991
+ expect(retVal.products[0].options.map(o => o.optionId)).toEqual([
992
+ 'TESTBDOPTION00001',
993
+ 'TESTTROPTION00001',
994
+ ]);
995
+ debugSpy.mockRestore();
996
+ });
997
+
998
+ it('omits multiple listed service codes', async () => {
999
+ const debugSpy = jest.spyOn(console, 'debug').mockImplementation(() => {});
1000
+ mockFullCatalog({ throwOnOpt: ['???AC????????????', '???TR????????????'] });
1001
+
1002
+ const retVal = await app.searchProductsForItinerary({
1003
+ axios,
1004
+ token,
1005
+ typeDefsAndQueries,
1006
+ payload: {
1007
+ searchInput: '',
1008
+ omitServiceCodes: ['AC', 'TR'],
1009
+ },
1010
+ });
1011
+
1012
+ expect(getOptionInfoCalls()).toEqual(['???BD????????????']);
1013
+ expect(debugSpy).toHaveBeenCalledWith(
1014
+ '[tourplan] Omitting service code(s) from full catalog product search: AC, TR',
1015
+ );
1016
+ expect(retVal.products[0].options.map(o => o.optionId)).toEqual([
1017
+ 'TESTBDOPTION00001',
1018
+ ]);
1019
+ debugSpy.mockRestore();
1020
+ });
1021
+
1022
+ it('coerces a single string omitServiceCodes value to an array', async () => {
1023
+ const debugSpy = jest.spyOn(console, 'debug').mockImplementation(() => {});
1024
+ mockFullCatalog({ throwOnOpt: ['???AC????????????'] });
1025
+
1026
+ const retVal = await app.searchProductsForItinerary({
1027
+ axios,
1028
+ token,
1029
+ typeDefsAndQueries,
1030
+ payload: {
1031
+ searchInput: '',
1032
+ omitServiceCodes: 'AC',
1033
+ },
1034
+ });
1035
+
1036
+ expect(getOptionInfoCalls()).toEqual([
1037
+ '???BD????????????',
1038
+ '???TR????????????',
1039
+ ]);
1040
+ expect(debugSpy).toHaveBeenCalledWith(
1041
+ '[tourplan] Omitting service code(s) from full catalog product search: AC',
1042
+ );
1043
+ expect(retVal.products[0].options.map(o => o.optionId)).toEqual([
1044
+ 'TESTBDOPTION00001',
1045
+ 'TESTTROPTION00001',
1046
+ ]);
1047
+ debugSpy.mockRestore();
1048
+ });
1049
+
1050
+ it('normalizes case and whitespace in omitServiceCodes', async () => {
1051
+ const debugSpy = jest.spyOn(console, 'debug').mockImplementation(() => {});
1052
+ mockFullCatalog({ throwOnOpt: ['???AC????????????'] });
1053
+
1054
+ const retVal = await app.searchProductsForItinerary({
1055
+ axios,
1056
+ token,
1057
+ typeDefsAndQueries,
1058
+ payload: {
1059
+ searchInput: '',
1060
+ omitServiceCodes: [' ac '],
1061
+ },
1062
+ });
1063
+
1064
+ expect(getOptionInfoCalls()).toEqual([
1065
+ '???BD????????????',
1066
+ '???TR????????????',
1067
+ ]);
1068
+ expect(debugSpy).toHaveBeenCalledWith(
1069
+ '[tourplan] Omitting service code(s) from full catalog product search: AC',
1070
+ );
1071
+ expect(retVal.products[0].options.map(o => o.optionId)).toEqual([
1072
+ 'TESTBDOPTION00001',
1073
+ 'TESTTROPTION00001',
1074
+ ]);
1075
+ debugSpy.mockRestore();
1076
+ });
1077
+
1078
+ it('splits a comma-separated omitServiceCodes string', async () => {
1079
+ const debugSpy = jest.spyOn(console, 'debug').mockImplementation(() => {});
1080
+ mockFullCatalog({ throwOnOpt: ['???AC????????????', '???TR????????????'] });
1081
+
1082
+ const retVal = await app.searchProductsForItinerary({
1083
+ axios,
1084
+ token,
1085
+ typeDefsAndQueries,
1086
+ payload: {
1087
+ searchInput: '',
1088
+ omitServiceCodes: 'AC, TR',
1089
+ },
1090
+ });
1091
+
1092
+ expect(getOptionInfoCalls()).toEqual(['???BD????????????']);
1093
+ expect(debugSpy).toHaveBeenCalledWith(
1094
+ '[tourplan] Omitting service code(s) from full catalog product search: AC, TR',
1095
+ );
1096
+ expect(retVal.products[0].options.map(o => o.optionId)).toEqual([
1097
+ 'TESTBDOPTION00001',
1098
+ ]);
1099
+ debugSpy.mockRestore();
1100
+ });
1101
+
1102
+ it('splits comma-separated entries inside an omitServiceCodes array', async () => {
1103
+ const debugSpy = jest.spyOn(console, 'debug').mockImplementation(() => {});
1104
+ mockFullCatalog({ throwOnOpt: ['???AC????????????', '???TR????????????'] });
1105
+
1106
+ const retVal = await app.searchProductsForItinerary({
1107
+ axios,
1108
+ token,
1109
+ typeDefsAndQueries,
1110
+ payload: {
1111
+ searchInput: '',
1112
+ omitServiceCodes: ['AC,SM', ' tr '],
1113
+ },
1114
+ });
1115
+
1116
+ expect(getOptionInfoCalls()).toEqual(['???BD????????????']);
1117
+ expect(debugSpy).toHaveBeenCalledWith(
1118
+ '[tourplan] Omitting service code(s) from full catalog product search: AC, SM, TR',
1119
+ );
1120
+ expect(retVal.products[0].options.map(o => o.optionId)).toEqual([
1121
+ 'TESTBDOPTION00001',
1122
+ ]);
1123
+ debugSpy.mockRestore();
1124
+ });
1125
+
1126
+ it('throws No products found when every service code is omitted', async () => {
1127
+ mockFullCatalog({
1128
+ throwOnOpt: [
1129
+ '???AC????????????',
1130
+ '???BD????????????',
1131
+ '???TR????????????',
1132
+ ],
1133
+ });
924
1134
 
925
- debugSpy.mockRestore();
1135
+ await expect(app.searchProductsForItinerary({
1136
+ axios,
1137
+ token,
1138
+ typeDefsAndQueries,
1139
+ payload: {
1140
+ searchInput: '',
1141
+ omitServiceCodes: ['AC', 'BD', 'TR'],
1142
+ },
1143
+ })).rejects.toThrow('No products found');
1144
+
1145
+ expect(getOptionInfoCalls()).toEqual([]);
1146
+ });
926
1147
  });
927
1148
 
928
1149
  describe('availability tests', () => {
@@ -22,8 +22,9 @@ const searchProductsForItinerary = async ({
22
22
  // lastUpdatedFrom is used to get options that were updated after a certain date in Tourplan
23
23
  // example: lastUpdatedFrom: '2024-04-22 05:17:57.427Z'
24
24
  lastUpdatedFrom,
25
- // skip Accommodation (AC) service code in full catalog search
26
- skipAccommodation,
25
+ // service codes to omit from full catalog search
26
+ // (e.g. ['AC'], 'AC', or 'AC,SM'); when empty/absent, get all
27
+ omitServiceCodes,
27
28
  },
28
29
  callTourplan,
29
30
  }) => {
@@ -60,7 +61,7 @@ const searchProductsForItinerary = async ({
60
61
  /*
61
62
  Full catalog path:
62
63
  1. getServiceCodes -> [AC, BD]
63
- 2. for each serviceCode getoptions (optionally skip AC when skipAccommodation is set)
64
+ 2. for each serviceCode getoptions (omitting all service codes in omitServiceCodes)
64
65
  3. convert them to ti2 products structure
65
66
  4. merge all products from all serviceCodes
66
67
  */
@@ -79,12 +80,26 @@ const searchProductsForItinerary = async ({
79
80
  });
80
81
  let serviceCodes = R.pathOr([], ['GetServicesReply', 'TPLServices', 'TPLService'], getServicesReply);
81
82
  if (!Array.isArray(serviceCodes)) serviceCodes = [serviceCodes];
82
- if (skipAccommodation) {
83
- console.debug('[tourplan] Accommodation (AC) service code will be skipped in full catalog product search');
83
+ let rawOmitCodes = [];
84
+ if (Array.isArray(omitServiceCodes)) {
85
+ rawOmitCodes = omitServiceCodes;
86
+ } else if (omitServiceCodes) {
87
+ rawOmitCodes = [omitServiceCodes];
88
+ }
89
+ const omitSet = new Set(
90
+ rawOmitCodes
91
+ .flatMap(code => String(code == null ? '' : code).split(','))
92
+ .map(code => code.trim().toUpperCase())
93
+ .filter(Boolean),
94
+ );
95
+ if (omitSet.size > 0) {
96
+ console.debug(
97
+ `[tourplan] Omitting service code(s) from full catalog product search: ${[...omitSet].join(', ')}`,
98
+ );
84
99
  }
85
100
  serviceCodes = serviceCodes
86
101
  .map(s => s.Code)
87
- .filter(code => !(skipAccommodation && code === 'AC'));
102
+ .filter(code => !omitSet.has(String(code).trim().toUpperCase()));
88
103
  await Promise.each(serviceCodes, async serviceCode => {
89
104
  const getOptionsModel = {
90
105
  OptionInfoRequest: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ti2-tourplan",
3
- "version": "1.0.129-beta.1",
3
+ "version": "1.0.129",
4
4
  "description": "Tourplan's TI2 Plugin",
5
5
  "main": "index.js",
6
6
  "scripts": {