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 +270 -49
- package/itinerary-products-search.js +21 -6
- package/package.json +1 -1
package/index.test.js
CHANGED
|
@@ -863,66 +863,287 @@ describe('search tests', () => {
|
|
|
863
863
|
expect(retVal).toMatchSnapshot();
|
|
864
864
|
});
|
|
865
865
|
|
|
866
|
-
|
|
867
|
-
const
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
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
|
-
|
|
883
|
-
.
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
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
|
|
899
|
-
return { OptionInfoReply: { Option:
|
|
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
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
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
|
-
|
|
915
|
-
.
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
26
|
-
|
|
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 (
|
|
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
|
-
|
|
83
|
-
|
|
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 => !(
|
|
102
|
+
.filter(code => !omitSet.has(String(code).trim().toUpperCase()));
|
|
88
103
|
await Promise.each(serviceCodes, async serviceCode => {
|
|
89
104
|
const getOptionsModel = {
|
|
90
105
|
OptionInfoRequest: {
|