@bytebrand/export-cars 1.26.28 → 1.26.30

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 (90) hide show
  1. package/README.md +100 -0
  2. package/dist/configs/index.d.ts +1 -0
  3. package/dist/configs/index.js +1 -0
  4. package/dist/configs/index.js.map +1 -1
  5. package/dist/configs/openai-ads.config.d.ts +22 -0
  6. package/dist/configs/openai-ads.config.js +56 -0
  7. package/dist/configs/openai-ads.config.js.map +1 -0
  8. package/dist/enums/export-type.enum.d.ts +2 -1
  9. package/dist/enums/export-type.enum.js +1 -0
  10. package/dist/enums/export-type.enum.js.map +1 -1
  11. package/dist/enums/image-type.enum.d.ts +2 -1
  12. package/dist/enums/image-type.enum.js +1 -0
  13. package/dist/enums/image-type.enum.js.map +1 -1
  14. package/dist/export.schema.d.ts +6 -2
  15. package/dist/export.schema.js +4 -0
  16. package/dist/export.schema.js.map +1 -1
  17. package/dist/exporters/index.d.ts +1 -0
  18. package/dist/exporters/index.js +1 -0
  19. package/dist/exporters/index.js.map +1 -1
  20. package/dist/exporters/openai-ads.exporter.d.ts +13 -0
  21. package/dist/exporters/openai-ads.exporter.js +91 -0
  22. package/dist/exporters/openai-ads.exporter.js.map +1 -0
  23. package/dist/formats/openai-ads-csv.builder.d.ts +15 -0
  24. package/dist/formats/openai-ads-csv.builder.js +48 -0
  25. package/dist/formats/openai-ads-csv.builder.js.map +1 -0
  26. package/dist/interfaces/openai-ads.interface.d.ts +35 -0
  27. package/dist/interfaces/openai-ads.interface.js +37 -0
  28. package/dist/interfaces/openai-ads.interface.js.map +1 -0
  29. package/dist/interfaces/params.interface.d.ts +10 -0
  30. package/dist/services/feed-guard.d.ts +44 -0
  31. package/dist/services/feed-guard.js +156 -0
  32. package/dist/services/feed-guard.js.map +1 -0
  33. package/dist/services/image-url-validator.service.d.ts +45 -0
  34. package/dist/services/image-url-validator.service.js +141 -0
  35. package/dist/services/image-url-validator.service.js.map +1 -0
  36. package/dist/services/images.service.d.ts +12 -0
  37. package/dist/services/images.service.js +46 -1
  38. package/dist/services/images.service.js.map +1 -1
  39. package/dist/services/index.d.ts +1 -0
  40. package/dist/services/index.js +1 -0
  41. package/dist/services/index.js.map +1 -1
  42. package/dist/services/openai-ads-feed-run.d.ts +74 -0
  43. package/dist/services/openai-ads-feed-run.js +151 -0
  44. package/dist/services/openai-ads-feed-run.js.map +1 -0
  45. package/dist/services/openai-ads-ledger.d.ts +19 -0
  46. package/dist/services/openai-ads-ledger.js +72 -0
  47. package/dist/services/openai-ads-ledger.js.map +1 -0
  48. package/dist/services/publication-gate.js +8 -8
  49. package/dist/transformers/openai-ads.transformer.d.ts +29 -0
  50. package/dist/transformers/openai-ads.transformer.js +166 -0
  51. package/dist/transformers/openai-ads.transformer.js.map +1 -0
  52. package/dist/transformers/openai-image-fields.d.ts +16 -0
  53. package/dist/transformers/openai-image-fields.js +34 -0
  54. package/dist/transformers/openai-image-fields.js.map +1 -0
  55. package/dist/tsconfig.build.tsbuildinfo +1 -1
  56. package/docs/openai-ads/AUDIT.md +235 -0
  57. package/docs/openai-ads/RUNBOOK.md +94 -0
  58. package/package.json +64 -64
  59. package/schema/feed-items.json +134 -134
  60. package/src/configs/config.ts +99 -99
  61. package/src/configs/index.ts +1 -0
  62. package/src/configs/openai-ads.config.ts +70 -0
  63. package/src/enums/export-type.enum.ts +2 -0
  64. package/src/enums/image-type.enum.ts +5 -0
  65. package/src/export.schema.ts +6 -0
  66. package/src/exporters/index.ts +1 -0
  67. package/src/exporters/openai-ads.exporter.ts +93 -0
  68. package/src/formats/openai-ads-csv.builder.ts +55 -0
  69. package/src/interfaces/openai-ads.interface.ts +64 -0
  70. package/src/interfaces/params.interface.ts +13 -0
  71. package/src/services/feed-guard.ts +219 -0
  72. package/src/services/image-url-validator.service.ts +181 -0
  73. package/src/services/images.service.ts +72 -0
  74. package/src/services/index.ts +1 -0
  75. package/src/services/openai-ads-feed-run.ts +226 -0
  76. package/src/services/openai-ads-ledger.ts +116 -0
  77. package/src/services/publication-gate.ts +119 -119
  78. package/src/transformers/openai-ads.transformer.ts +243 -0
  79. package/src/transformers/openai-image-fields.ts +60 -0
  80. package/test/clean-images.test.ts +86 -0
  81. package/test/fixtures/images-service.harness.ts +32 -0
  82. package/test/fixtures/images.golden.json +218 -0
  83. package/test/fixtures/imbo-images.fixture.ts +77 -0
  84. package/test/fixtures/openai-ads.fixture.ts +89 -0
  85. package/test/image-url-validator.test.ts +110 -0
  86. package/test/images-regression.test.ts +23 -0
  87. package/test/openai-ads-feed-run.test.ts +278 -0
  88. package/test/openai-ads-mapping.test.ts +157 -0
  89. package/test/openai-image-fields.test.ts +99 -0
  90. package/test/publication-gate.test.ts +86 -86
@@ -0,0 +1,218 @@
1
+ {
2
+ "full": {
3
+ "defaultWithPlaceholders": [
4
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/extRight0001/large",
5
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/extCowl00001/large",
6
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/extLeft00001/large",
7
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/extSide00001/large",
8
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/extBack00001/large",
9
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/extOpened001/large",
10
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/conext000001/large",
11
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/engine000001/large",
12
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/intDash00001/large",
13
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/intSeats0001/large",
14
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/intTrunk0001/large",
15
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/seatHeat0001/large",
16
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/featRims0001/large",
17
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/simple000001/large",
18
+ "https://images.auto.de/carimage/noimage/7pNvWELuWhZX/large-cached.jpeg",
19
+ "https://images.auto.de/carimage/noimage/GvyXmrpaf1D6/large-cached.jpeg",
20
+ "https://images.auto.de/carimage/noimage/pFyfBsqBHEso/large-cached.jpeg"
21
+ ],
22
+ "defaultIgnorePlaceholders": [
23
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/extRight0001/large",
24
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/extCowl00001/large",
25
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/extLeft00001/large",
26
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/extSide00001/large",
27
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/extBack00001/large",
28
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/extOpened001/large",
29
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/conext000001/large",
30
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/engine000001/large",
31
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/intDash00001/large",
32
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/intSeats0001/large",
33
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/intTrunk0001/large",
34
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/seatHeat0001/large",
35
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/featRims0001/large",
36
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/simple000001/large"
37
+ ],
38
+ "marketing": [
39
+ "https://images.auto.de/generated_images/2/c/e/2ce1d71f-851f-4123-ae16-187ddd7923c5/criteo-channel-extRight0001.jpg",
40
+ "https://images.auto.de/generated_images/2/c/e/2ce1d71f-851f-4123-ae16-187ddd7923c5/criteo-channel-extBack00001.jpg",
41
+ "https://images.auto.de/generated_images/2/c/e/2ce1d71f-851f-4123-ae16-187ddd7923c5/criteo-channel-intDash00001.jpg",
42
+ "https://images.auto.de/carimage/noimage/7pNvWELuWhZX/large-cached.jpeg",
43
+ "https://images.auto.de/carimage/noimage/GvyXmrpaf1D6/large-cached.jpeg",
44
+ "https://images.auto.de/carimage/noimage/pFyfBsqBHEso/large-cached.jpeg"
45
+ ],
46
+ "googleExtInt": {
47
+ "extMain": [
48
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/extRight0001/small",
49
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/extLeft00001/small",
50
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/extOpened001/small"
51
+ ],
52
+ "ext": [
53
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/extCowl00001/small",
54
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/extSide00001/small",
55
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/extBack00001/small"
56
+ ],
57
+ "int": [
58
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/intDash00001/small",
59
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/intSeats0001/small",
60
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/intTrunk0001/small"
61
+ ],
62
+ "feat": [
63
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/conext000001/small",
64
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/engine000001/small",
65
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/seatHeat0001/small",
66
+ "https://images.auto.de/carimage/2ce1d71f-851f-4123-ae16-187ddd7923c5/featRims0001/small"
67
+ ]
68
+ }
69
+ },
70
+ "leftOnly": {
71
+ "defaultWithPlaceholders": [
72
+ "https://images.auto.de/carimage/3703a23a-654d-426e-801e-beeffcc1e9c8/extLeft00002/large",
73
+ "https://images.auto.de/carimage/3703a23a-654d-426e-801e-beeffcc1e9c8/extBack00002/large",
74
+ "https://images.auto.de/carimage/3703a23a-654d-426e-801e-beeffcc1e9c8/intDash00002/large",
75
+ "https://images.auto.de/carimage/noimage/7pNvWELuWhZX/large-cached.jpeg",
76
+ "https://images.auto.de/carimage/noimage/GvyXmrpaf1D6/large-cached.jpeg",
77
+ "https://images.auto.de/carimage/noimage/pFyfBsqBHEso/large-cached.jpeg"
78
+ ],
79
+ "defaultIgnorePlaceholders": [
80
+ "https://images.auto.de/carimage/3703a23a-654d-426e-801e-beeffcc1e9c8/extLeft00002/large",
81
+ "https://images.auto.de/carimage/3703a23a-654d-426e-801e-beeffcc1e9c8/extBack00002/large",
82
+ "https://images.auto.de/carimage/3703a23a-654d-426e-801e-beeffcc1e9c8/intDash00002/large"
83
+ ],
84
+ "marketing": [
85
+ "https://images.auto.de/generated_images/3/7/0/3703a23a-654d-426e-801e-beeffcc1e9c8/criteo-channel-extLeft00002.jpg",
86
+ "https://images.auto.de/generated_images/3/7/0/3703a23a-654d-426e-801e-beeffcc1e9c8/criteo-channel-extBack00002.jpg",
87
+ "https://images.auto.de/generated_images/3/7/0/3703a23a-654d-426e-801e-beeffcc1e9c8/criteo-channel-intDash00002.jpg",
88
+ "https://images.auto.de/carimage/noimage/7pNvWELuWhZX/large-cached.jpeg",
89
+ "https://images.auto.de/carimage/noimage/GvyXmrpaf1D6/large-cached.jpeg",
90
+ "https://images.auto.de/carimage/noimage/pFyfBsqBHEso/large-cached.jpeg"
91
+ ],
92
+ "googleExtInt": {
93
+ "extMain": [
94
+ "https://images.auto.de/carimage/3703a23a-654d-426e-801e-beeffcc1e9c8/extLeft00002/small"
95
+ ],
96
+ "ext": [
97
+ "https://images.auto.de/carimage/3703a23a-654d-426e-801e-beeffcc1e9c8/extBack00002/small"
98
+ ],
99
+ "int": [
100
+ "https://images.auto.de/carimage/3703a23a-654d-426e-801e-beeffcc1e9c8/intDash00002/small"
101
+ ],
102
+ "feat": []
103
+ }
104
+ },
105
+ "noHero": {
106
+ "defaultWithPlaceholders": [
107
+ "https://images.auto.de/carimage/2c91c177-ddd3-4224-8b04-bdb96bf1b64c/extBack00003/large",
108
+ "https://images.auto.de/carimage/2c91c177-ddd3-4224-8b04-bdb96bf1b64c/intDash00003/large",
109
+ "https://images.auto.de/carimage/2c91c177-ddd3-4224-8b04-bdb96bf1b64c/featRims0003/large",
110
+ "https://images.auto.de/carimage/noimage/7pNvWELuWhZX/large-cached.jpeg",
111
+ "https://images.auto.de/carimage/noimage/GvyXmrpaf1D6/large-cached.jpeg",
112
+ "https://images.auto.de/carimage/noimage/pFyfBsqBHEso/large-cached.jpeg"
113
+ ],
114
+ "defaultIgnorePlaceholders": [
115
+ "https://images.auto.de/carimage/2c91c177-ddd3-4224-8b04-bdb96bf1b64c/extBack00003/large",
116
+ "https://images.auto.de/carimage/2c91c177-ddd3-4224-8b04-bdb96bf1b64c/intDash00003/large",
117
+ "https://images.auto.de/carimage/2c91c177-ddd3-4224-8b04-bdb96bf1b64c/featRims0003/large"
118
+ ],
119
+ "marketing": [
120
+ "https://images.auto.de/generated_images/2/c/9/2c91c177-ddd3-4224-8b04-bdb96bf1b64c/criteo-channel-extBack00003.jpg",
121
+ "https://images.auto.de/generated_images/2/c/9/2c91c177-ddd3-4224-8b04-bdb96bf1b64c/criteo-channel-intDash00003.jpg",
122
+ "https://images.auto.de/carimage/noimage/7pNvWELuWhZX/large-cached.jpeg",
123
+ "https://images.auto.de/carimage/noimage/GvyXmrpaf1D6/large-cached.jpeg",
124
+ "https://images.auto.de/carimage/noimage/pFyfBsqBHEso/large-cached.jpeg"
125
+ ],
126
+ "googleExtInt": {
127
+ "extMain": [],
128
+ "ext": [
129
+ "https://images.auto.de/carimage/2c91c177-ddd3-4224-8b04-bdb96bf1b64c/extBack00003/small"
130
+ ],
131
+ "int": [
132
+ "https://images.auto.de/carimage/2c91c177-ddd3-4224-8b04-bdb96bf1b64c/intDash00003/small"
133
+ ],
134
+ "feat": [
135
+ "https://images.auto.de/carimage/2c91c177-ddd3-4224-8b04-bdb96bf1b64c/featRims0003/small"
136
+ ]
137
+ }
138
+ },
139
+ "duplicates": {
140
+ "defaultWithPlaceholders": [
141
+ "https://images.auto.de/carimage/2e101cac-fb72-4cd7-a598-2ebba983a4f9/extRight0004/large",
142
+ "https://images.auto.de/carimage/2e101cac-fb72-4cd7-a598-2ebba983a4f9/extRight0004/large",
143
+ "https://images.auto.de/carimage/2e101cac-fb72-4cd7-a598-2ebba983a4f9/extRight0005/large",
144
+ "https://images.auto.de/carimage/2e101cac-fb72-4cd7-a598-2ebba983a4f9/intDash00004/large",
145
+ "https://images.auto.de/carimage/noimage/7pNvWELuWhZX/large-cached.jpeg",
146
+ "https://images.auto.de/carimage/noimage/GvyXmrpaf1D6/large-cached.jpeg",
147
+ "https://images.auto.de/carimage/noimage/pFyfBsqBHEso/large-cached.jpeg"
148
+ ],
149
+ "defaultIgnorePlaceholders": [
150
+ "https://images.auto.de/carimage/2e101cac-fb72-4cd7-a598-2ebba983a4f9/extRight0004/large",
151
+ "https://images.auto.de/carimage/2e101cac-fb72-4cd7-a598-2ebba983a4f9/extRight0004/large",
152
+ "https://images.auto.de/carimage/2e101cac-fb72-4cd7-a598-2ebba983a4f9/extRight0005/large",
153
+ "https://images.auto.de/carimage/2e101cac-fb72-4cd7-a598-2ebba983a4f9/intDash00004/large"
154
+ ],
155
+ "marketing": [
156
+ "https://images.auto.de/generated_images/2/e/1/2e101cac-fb72-4cd7-a598-2ebba983a4f9/criteo-channel-extRight0005.jpg",
157
+ "https://images.auto.de/generated_images/2/e/1/2e101cac-fb72-4cd7-a598-2ebba983a4f9/criteo-channel-intDash00004.jpg",
158
+ "https://images.auto.de/carimage/noimage/7pNvWELuWhZX/large-cached.jpeg",
159
+ "https://images.auto.de/carimage/noimage/GvyXmrpaf1D6/large-cached.jpeg",
160
+ "https://images.auto.de/carimage/noimage/pFyfBsqBHEso/large-cached.jpeg"
161
+ ],
162
+ "googleExtInt": {
163
+ "extMain": [
164
+ "https://images.auto.de/carimage/2e101cac-fb72-4cd7-a598-2ebba983a4f9/extRight0004/small",
165
+ "https://images.auto.de/carimage/2e101cac-fb72-4cd7-a598-2ebba983a4f9/extRight0004/small",
166
+ "https://images.auto.de/carimage/2e101cac-fb72-4cd7-a598-2ebba983a4f9/extRight0005/small"
167
+ ],
168
+ "ext": [],
169
+ "int": [
170
+ "https://images.auto.de/carimage/2e101cac-fb72-4cd7-a598-2ebba983a4f9/intDash00004/small"
171
+ ],
172
+ "feat": []
173
+ }
174
+ },
175
+ "onlyUnknown": {
176
+ "defaultWithPlaceholders": [
177
+ "https://images.auto.de/carimage/61889cf8-2193-4c1e-8cdd-521ca2d38c9a/simple000006/large",
178
+ "https://images.auto.de/carimage/61889cf8-2193-4c1e-8cdd-521ca2d38c9a/promo0000006/large",
179
+ "https://images.auto.de/carimage/noimage/7pNvWELuWhZX/large-cached.jpeg",
180
+ "https://images.auto.de/carimage/noimage/GvyXmrpaf1D6/large-cached.jpeg",
181
+ "https://images.auto.de/carimage/noimage/pFyfBsqBHEso/large-cached.jpeg"
182
+ ],
183
+ "defaultIgnorePlaceholders": [
184
+ "https://images.auto.de/carimage/61889cf8-2193-4c1e-8cdd-521ca2d38c9a/simple000006/large",
185
+ "https://images.auto.de/carimage/61889cf8-2193-4c1e-8cdd-521ca2d38c9a/promo0000006/large"
186
+ ],
187
+ "marketing": [
188
+ "https://images.auto.de/carimage/noimage/7pNvWELuWhZX/large-cached.jpeg",
189
+ "https://images.auto.de/carimage/noimage/GvyXmrpaf1D6/large-cached.jpeg",
190
+ "https://images.auto.de/carimage/noimage/pFyfBsqBHEso/large-cached.jpeg"
191
+ ],
192
+ "googleExtInt": {
193
+ "extMain": [],
194
+ "ext": [],
195
+ "int": [],
196
+ "feat": []
197
+ }
198
+ },
199
+ "empty": {
200
+ "defaultWithPlaceholders": [
201
+ "https://images.auto.de/carimage/noimage/7pNvWELuWhZX/large-cached.jpeg",
202
+ "https://images.auto.de/carimage/noimage/GvyXmrpaf1D6/large-cached.jpeg",
203
+ "https://images.auto.de/carimage/noimage/pFyfBsqBHEso/large-cached.jpeg"
204
+ ],
205
+ "defaultIgnorePlaceholders": [
206
+ "https://images.auto.de/carimage/noimage/7pNvWELuWhZX/large-cached.jpeg",
207
+ "https://images.auto.de/carimage/noimage/GvyXmrpaf1D6/large-cached.jpeg",
208
+ "https://images.auto.de/carimage/noimage/pFyfBsqBHEso/large-cached.jpeg"
209
+ ],
210
+ "marketing": null,
211
+ "googleExtInt": {
212
+ "extMain": [],
213
+ "ext": [],
214
+ "int": [],
215
+ "feat": []
216
+ }
217
+ }
218
+ }
@@ -0,0 +1,77 @@
1
+ import { ImboImageResponseInterface } from '@bytebrand/chunkable';
2
+
3
+ // Imbo image lists as CarBasicDataCarsLoader hands them to the transformers (newest first, as Imbo
4
+ // returns them). Codes mirror real image_code_predicted labels, incl. the "-suffix" form.
5
+ const img = (imageIdentifier: string, imageCodePredicted?: string): ImboImageResponseInterface => ({
6
+ imageIdentifier,
7
+ imageCodePredicted,
8
+ });
9
+
10
+ export const IMBO_FIXTURES: Record<string, { carId: string; images: ImboImageResponseInterface[] }> = {
11
+ full: {
12
+ carId: '2ce1d71f-851f-4123-ae16-187ddd7923c5',
13
+ images: [
14
+ img('intDash00001', 'int_f_ipanel1'),
15
+ img('featRims0001', 'ext_feat_rims'),
16
+ img('extBack00001', 'ext_b_main'),
17
+ img('simple000001', 'simple_01'),
18
+ img('extLeft00001', 'ext_f_left'),
19
+ img('conext000001', 'conext_f_main'),
20
+ img('intSeats0001', 'int_f_driverseats-1'),
21
+ img('extCowl00001', 'ext_f_cowl'),
22
+ img('extOpened001', 'ext_f_main_opened'),
23
+ img('engine000001', 'ext_feat_engine'),
24
+ img('seatHeat0001', 'int_feat_seatheating'),
25
+ img('extRight0001', 'ext_f_right'),
26
+ img('extSide00001', 'ext_l_main'),
27
+ img('intTrunk0001', 'int_b_trunk'),
28
+ ],
29
+ },
30
+ leftOnly: {
31
+ carId: '3703a23a-654d-426e-801e-beeffcc1e9c8',
32
+ images: [
33
+ img('intDash00002', 'int_f_ipanel1'),
34
+ img('extBack00002', 'ext_b_main'),
35
+ img('extLeft00002', 'ext_f_left'),
36
+ ],
37
+ },
38
+ noHero: {
39
+ carId: '2c91c177-ddd3-4224-8b04-bdb96bf1b64c',
40
+ images: [
41
+ img('extBack00003', 'ext_b_main'),
42
+ img('intDash00003', 'int_f_ipanel1'),
43
+ img('featRims0003', 'ext_feat_rims'),
44
+ ],
45
+ },
46
+ duplicates: {
47
+ carId: '2e101cac-fb72-4cd7-a598-2ebba983a4f9',
48
+ images: [
49
+ img('extRight0004', 'ext_f_right'),
50
+ img('extRight0004', 'ext_f_right'),
51
+ img('extRight0005', 'ext_f_right'),
52
+ img('intDash00004', 'int_f_ipanel1'),
53
+ ],
54
+ },
55
+ onlyUnknown: {
56
+ carId: '61889cf8-2193-4c1e-8cdd-521ca2d38c9a',
57
+ images: [img('simple000006', 'simple_01'), img('promo0000006', 'promo_slide')],
58
+ },
59
+ empty: {
60
+ carId: '0b0999c6-2f02-4bc3-8852-2307e8a0232f',
61
+ images: [],
62
+ },
63
+ };
64
+
65
+ // Same shape as a stored MarketingImageConfig (criteo-like) for getMarketingImages.
66
+ export const MARKETING_CONFIG_FIXTURE = {
67
+ channelCode: 'criteo channel',
68
+ imageExtension: 'jpg',
69
+ marketingImages: [
70
+ { imageCodes: ['ext_f_right', 'ext_f_left'] },
71
+ { imageCodes: ['int_f_ipanel1'] },
72
+ { imageCodes: ['ext_b_main'] },
73
+ ],
74
+ };
75
+
76
+ export const cloneImages = (images: ImboImageResponseInterface[]): ImboImageResponseInterface[] =>
77
+ images.map(i => ({ ...i }));
@@ -0,0 +1,89 @@
1
+ import { ImboImageResponseInterface } from '@bytebrand/chunkable';
2
+
3
+ // Minimal RFC 4180 reader for the tests: whole file -> rows of cells (quoted newlines kept).
4
+ export const parseCsv = (text: string): string[][] => {
5
+ const rows: string[][] = [];
6
+ let row: string[] = [];
7
+ let cell = '';
8
+ let quoted = false;
9
+ for (let i = 0; i < text.length; i++) {
10
+ const c = text[i];
11
+ if (quoted) {
12
+ if (c === '"' && text[i + 1] === '"') { cell += '"'; i++; } else if (c === '"') { quoted = false; } else { cell += c; }
13
+ } else if (c === '"') {
14
+ quoted = true;
15
+ } else if (c === ',') {
16
+ row.push(cell); cell = '';
17
+ } else if (c === '\n') {
18
+ row.push(cell); rows.push(row); row = []; cell = '';
19
+ } else {
20
+ cell += c;
21
+ }
22
+ }
23
+ if (cell || row.length) { row.push(cell); rows.push(row); }
24
+ return rows;
25
+ };
26
+
27
+ export const csvObjects = (text: string): Record<string, string>[] => {
28
+ const [header, ...rows] = parseCsv(text);
29
+ return rows.map(cells => Object.fromEntries(header.map((name, i) => [name, cells[i]])));
30
+ };
31
+
32
+ // German labels the TranslatorExtended returns for these selectors (checked against
33
+ // @bytebrand/i18n-dictionaries); anything else comes back as the key itself, like the real one.
34
+ const LABELS: Record<string, string> = {
35
+ selector_fuel_petrol: 'Benzin',
36
+ selector_fuel_diesel: 'Diesel',
37
+ selector_gearbox_manualShift: 'Manuell',
38
+ selector_gearbox_automatic: 'Automatik',
39
+ selector_category_sedan: 'Limousine',
40
+ selector_category_combi: 'Kombi',
41
+ selector_exteriorColor_black: 'Schwarz',
42
+ selector_exteriorColor_grey: 'Grau',
43
+ selector_unknown: 'Unbekannt',
44
+ };
45
+ export const translate = (key: string): string => LABELS[key] ?? key;
46
+
47
+ export const NOW = new Date('2026-10-12T08:00:00Z');
48
+ const MAR_2021 = Date.UTC(2021, 2, 15) / 1000;
49
+
50
+ export const car = (id: string, overrides: Record<string, any> = {}): any => {
51
+ const base: any = {
52
+ _id: id,
53
+ mainData: {
54
+ make: 'Volkswagen',
55
+ model: 'Golf',
56
+ subModel: '1.5 TSI Life',
57
+ condition: 'selector_condition_used',
58
+ firstRegistration: MAR_2021,
59
+ mileage: 45000,
60
+ category: 'selector_category_sedan',
61
+ },
62
+ metaData: { ownerId: 'dealer-1', imagesCount: 2, vehicleId: 'ABC123' },
63
+ price: { currentSalesPrice: 17990 },
64
+ rate: { monthlyInstallment: 199, paybackPeriod: 48 },
65
+ consumption: { fuel: 'selector_fuel_petrol' },
66
+ driveSuspension: { gearbox: 'selector_gearbox_manualShift' },
67
+ engineData: { powerKW: 110, powerPS: 150 },
68
+ exterior: { exteriorColor: 'selector_exteriorColor_black' },
69
+ offer: { offerAvailability: 'selector_unknown' },
70
+ state: { isOnline: true },
71
+ };
72
+ for (const [path, value] of Object.entries(overrides)) {
73
+ const keys = path.split('.');
74
+ let target = base;
75
+ keys.slice(0, -1).forEach((key) => { target[key] = target[key] ?? {}; target = target[key]; });
76
+ if (value === undefined) {
77
+ delete target[keys[keys.length - 1]];
78
+ } else {
79
+ target[keys[keys.length - 1]] = value;
80
+ }
81
+ }
82
+ return base;
83
+ };
84
+
85
+ export const images = (prefix: string): ImboImageResponseInterface[] => [
86
+ { imageIdentifier: `${prefix}Back`, imageCodePredicted: 'ext_b_main' },
87
+ { imageIdentifier: `${prefix}Right`, imageCodePredicted: 'ext_f_right' },
88
+ { imageIdentifier: `${prefix}Dash`, imageCodePredicted: 'int_f_ipanel1' },
89
+ ];
@@ -0,0 +1,110 @@
1
+ // Run: npm test (ts-node, no test framework needed)
2
+ // Local stub image host — no real network. requireHttps is switched off only for the stub server.
3
+ import * as assert from 'node:assert/strict';
4
+ import { createServer, IncomingMessage, ServerResponse } from 'http';
5
+ import { AddressInfo } from 'net';
6
+ import { ImageUrlRejectReason as R, ImageUrlValidator } from '../src/services/image-url-validator.service';
7
+
8
+ const hits: Record<string, number> = {};
9
+ const JPEG = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10]);
10
+
11
+ const routes: Record<string, (req: IncomingMessage, res: ServerResponse) => void> = {
12
+ '/ok.jpg': (_, res) => { res.writeHead(200, { 'Content-Type': 'image/jpeg' }); res.end(JPEG); },
13
+ '/ok.png': (_, res) => { res.writeHead(200, { 'Content-Type': 'image/png' }); res.end(); },
14
+ '/photo.webp': (_, res) => { res.writeHead(200, { 'Content-Type': 'image/webp' }); res.end(); },
15
+ '/page.html': (_, res) => { res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end('<html>'); },
16
+ // images.auto.de answers unknown images exactly like this: 404 with a placeholder JPEG body
17
+ '/missing.jpg': (_, res) => { res.writeHead(404, { 'Content-Type': 'image/jpeg' }); res.end(JPEG); },
18
+ '/forbidden.jpg': (_, res) => { res.writeHead(403, { 'Content-Type': 'text/html' }); res.end('Just a moment...'); },
19
+ '/error.jpg': (_, res) => { res.writeHead(503); res.end(); },
20
+ '/moved.jpg': (_, res) => { res.writeHead(302, { Location: '/ok.jpg' }); res.end(); },
21
+ '/loop.jpg': (_, res) => { res.writeHead(302, { Location: '/loop.jpg' }); res.end(); },
22
+ '/slow.jpg': (_, res) => { setTimeout(() => { res.writeHead(200, { 'Content-Type': 'image/jpeg' }); res.end(); }, 400); },
23
+ '/no-head.jpg': (req, res) => {
24
+ if (req.method === 'HEAD') { res.writeHead(405); res.end(); return; }
25
+ res.writeHead(200, { 'Content-Type': 'image/jpeg' }); res.end(JPEG);
26
+ },
27
+ '/carimage/noimage/7pNvWELuWhZX/large-cached.jpeg': (_, res) => { res.writeHead(200, { 'Content-Type': 'image/jpeg' }); res.end(JPEG); },
28
+ };
29
+
30
+ const server = createServer((req, res) => {
31
+ const path = req.url.split('?')[0];
32
+ hits[path] = (hits[path] ?? 0) + 1;
33
+ (routes[path] ?? ((_, r) => { r.writeHead(404); r.end(); }))(req, res);
34
+ });
35
+
36
+ (async () => {
37
+ await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
38
+ const base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
39
+ const validator = (options = {}) => new ImageUrlValidator({ requireHttps: false, timeoutMs: 150, ...options });
40
+ const expectReject = async (path: string, reason: R, status?: number) => {
41
+ const check = await validator().validate(`${base}${path}`);
42
+ assert.equal(check.ok, false);
43
+ assert.equal(check.reason, reason);
44
+ if (status) assert.equal(check.status, status);
45
+ };
46
+
47
+ const cases: Array<[string, () => Promise<void>]> = [
48
+ ['200 image/jpeg and image/png are valid', async () => {
49
+ assert.equal((await validator().validate(`${base}/ok.jpg`)).ok, true);
50
+ assert.equal((await validator().validate(`${base}/ok.png`)).ok, true);
51
+ }],
52
+ ['404 with an image/jpeg placeholder body is rejected on status', () => expectReject('/missing.jpg', R.httpStatus, 404)],
53
+ ['403 (WAF / challenge) is rejected', () => expectReject('/forbidden.jpg', R.httpStatus, 403)],
54
+ ['5xx is rejected', () => expectReject('/error.jpg', R.httpStatus, 503)],
55
+ ['wrong MIME (HTML) is rejected', () => expectReject('/page.html', R.wrongMime)],
56
+ ['WebP is rejected unless the partner allows it', async () => {
57
+ await expectReject('/photo.webp', R.wrongMime);
58
+ assert.equal((await validator({ allowedMimeTypes: ['image/webp'] }).validate(`${base}/photo.webp`)).ok, true);
59
+ }],
60
+ ['a redirect within the limit is followed and the final URL kept', async () => {
61
+ const check = await validator().validate(`${base}/moved.jpg`);
62
+ assert.equal(check.ok, true);
63
+ assert.equal(check.finalUrl, `${base}/ok.jpg`);
64
+ }],
65
+ ['redirect loop is rejected', () => expectReject('/loop.jpg', R.tooManyRedirects)],
66
+ ['timeout is rejected', () => expectReject('/slow.jpg', R.timeout)],
67
+ ['HEAD not allowed (405) falls back to GET', async () => {
68
+ assert.equal((await validator().validate(`${base}/no-head.jpg`)).ok, true);
69
+ }],
70
+ ['placeholder URL is rejected without any request', async () => {
71
+ await expectReject('/carimage/noimage/7pNvWELuWhZX/large-cached.jpeg', R.placeholder);
72
+ assert.equal(hits['/carimage/noimage/7pNvWELuWhZX/large-cached.jpeg'], undefined);
73
+ }],
74
+ ['non-HTTPS, credentials and relative URLs are rejected without a request', async () => {
75
+ const strict = new ImageUrlValidator();
76
+ assert.equal((await strict.validate(`${base}/ok.png`)).reason, R.notHttps);
77
+ assert.equal((await strict.validate('https://user:secret@images.auto.de/carimage/a/b/large-cached.jpeg')).reason, R.credentials);
78
+ assert.equal((await strict.validate('/carimage/a/b/large-cached.jpeg')).reason, R.invalidUrl);
79
+ assert.equal((await strict.validate('ftp://images.auto.de/a.jpg')).reason, R.invalidUrl);
80
+ }],
81
+ ['each URL is probed once per run (cached) and counted once', async () => {
82
+ const v = validator();
83
+ const before = hits['/ok.jpg'];
84
+ await v.validateAll([`${base}/ok.jpg`, `${base}/ok.jpg`, `${base}/missing.jpg`]);
85
+ await v.validate(`${base}/ok.jpg`);
86
+ assert.equal(hits['/ok.jpg'] - before, 1);
87
+ assert.deepEqual(v.stats, { ok: 1, [R.httpStatus]: 1 });
88
+ }],
89
+ ['probe budget: URLs beyond maxProbes are reported, not fetched', async () => {
90
+ const v = validator({ maxProbes: 1 });
91
+ const [first, second] = await v.validateAll([`${base}/ok.jpg`, `${base}/ok.png`]);
92
+ assert.equal(first.ok, true);
93
+ assert.equal(second.reason, R.budgetExhausted);
94
+ }],
95
+ ];
96
+
97
+ let failed = 0;
98
+ for (const [name, fn] of cases) {
99
+ try {
100
+ await fn();
101
+ console.log(`ok ${name}`);
102
+ } catch (e) {
103
+ failed += 1;
104
+ console.error(`FAIL ${name}\n ${e.message}`);
105
+ }
106
+ }
107
+ (server as any).closeAllConnections?.(); // Node ≥18.2; @types/node here is older
108
+ server.close();
109
+ if (failed) process.exit(1);
110
+ })();
@@ -0,0 +1,23 @@
1
+ // Run: npm test (ts-node, no test framework needed)
2
+ // ADV-336 regression lock: the image URLs every existing partner export (Google, Meta-marketing,
3
+ // default partners) builds must stay byte-identical. images.golden.json was recorded from the
4
+ // ImagesService BEFORE the clean profile was added — never regenerate it to make this test pass.
5
+ import * as assert from 'node:assert/strict';
6
+ import { readFileSync } from 'fs';
7
+ import { snapshotExistingImageOutputs } from './fixtures/images-service.harness';
8
+
9
+ const golden = JSON.parse(readFileSync(`${__dirname}/fixtures/images.golden.json`, 'utf8'));
10
+
11
+ snapshotExistingImageOutputs().then((actual) => {
12
+ let failed = 0;
13
+ for (const name of Object.keys(golden)) {
14
+ try {
15
+ assert.deepEqual(actual[name], golden[name]);
16
+ console.log(`ok existing image outputs unchanged: ${name}`);
17
+ } catch (e) {
18
+ failed += 1;
19
+ console.error(`FAIL existing image outputs changed: ${name}\n ${e.message}`);
20
+ }
21
+ }
22
+ if (failed) process.exit(1);
23
+ });