@tillhub/schemas 6.455.0 → 6.457.0

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.
@@ -374,6 +374,49 @@ module.exports = oneOf({
374
374
  }
375
375
  })
376
376
  }
377
+ }),
378
+ press_item_barcode: oneOf({
379
+ description: 'German press products carry their retail price and VAT class inside the EAN-13 barcode, so they can be sold without being maintained in the catalog.',
380
+ type: 'object',
381
+ additionalProperties: false,
382
+ properties: {
383
+ enabled: {
384
+ description: 'Specifies if the feature is available. When false, behaviour is identical to a client without the feature.',
385
+ type: 'boolean',
386
+ default: false
387
+ },
388
+ lookup_priority: {
389
+ description: 'Which side is asked first for a scanned code, i.e. which article carries the line. \'catalog_first\' searches the catalog and decodes only what it does not find, \'barcode_first\' decodes before searching. An unrecognised value falls back to the default.',
390
+ type: 'string',
391
+ enum: [
392
+ 'catalog_first',
393
+ 'barcode_first'
394
+ ],
395
+ default: 'catalog_first'
396
+ },
397
+ price_source_on_catalog_hit: {
398
+ description: 'Which price is booked when a decodable press code also matches a catalog article. \'barcode\' charges the encoded amount and uses the article\'s own price only as the value it is compared against, \'catalog\' charges the article\'s own price and falls back to the encoded amount when it has none. A disagreement between the two is logged under either setting. The field is inert wherever no catalog hit occurs, which includes every scan under \'barcode_first\' - that combination is contradictory but not an error and must not be rejected. An unrecognised value falls back to the default.',
399
+ type: 'string',
400
+ enum: [
401
+ 'barcode',
402
+ 'catalog'
403
+ ],
404
+ default: 'barcode'
405
+ },
406
+ products: {
407
+ description: 'Maps a press prefix to the uuid of the fallback article that supplies VAT and accounting information for that prefix. The supported prefixes are the keys, not a fixed set, so adding one needs no client release. Enabled with an empty map is equivalent to disabled.',
408
+ type: 'object',
409
+ additionalProperties: false,
410
+ patternProperties: {
411
+ '^[0-9]{3}$': {
412
+ description: 'Uuid of the fallback article for this press prefix.',
413
+ type: 'string',
414
+ format: 'uuid'
415
+ }
416
+ },
417
+ default: {}
418
+ }
419
+ }
377
420
  })
378
421
  }
379
422
  })
@@ -1,262 +1,257 @@
1
1
  const { oneOf } = require('../../../helpers/payload-or-null')
2
2
 
3
3
  module.exports = {
4
- anyOf: [
5
- {
6
- type: 'null'
7
- },
8
- {
4
+ type: ['object', 'null'],
5
+ additionalProperties: false,
6
+ properties: {
7
+ scanner_apis: {
9
8
  type: 'object',
10
- additionalProperties: false,
9
+ default: {},
11
10
  properties: {
12
- scanner_apis: {
11
+ zebra: {
13
12
  type: 'object',
13
+ default: {},
14
14
  properties: {
15
- zebra: {
16
- type: 'object',
17
- properties: {
18
- enabled: {
19
- type: 'boolean',
20
- default: false
21
- },
22
- background_release_seconds: {
23
- description: 'The time in seconds that the scanner will stay connected when the app is send to the background (for other apps to grab afterwards)',
24
- type: 'integer',
25
- example: 15,
26
- minimum: 0,
27
- maximum: 30, // to be on the safe side of Apple
28
- default: 25
29
- }
30
- }
15
+ enabled: {
16
+ type: 'boolean',
17
+ default: true
31
18
  },
32
- epson: {
33
- type: 'object',
34
- properties: {
35
- enabled: {
36
- type: 'boolean',
37
- default: true
38
- },
39
- background_release_seconds: {
40
- description: 'The time in seconds that the scanner will stay connected when the app is send to the background (for other apps to grab afterwards)',
41
- type: 'integer',
42
- example: 15,
43
- minimum: 0,
44
- maximum: 30, // to be on the safe side of Apple
45
- default: 25
46
- }
47
- }
19
+ background_release_seconds: {
20
+ description: 'The time in seconds that the scanner will stay connected when the app is send to the background (for other apps to grab afterwards)',
21
+ type: 'integer',
22
+ example: 15,
23
+ minimum: 0,
24
+ maximum: 30, // to be on the safe side of Apple
25
+ default: 25
48
26
  }
49
27
  }
50
28
  },
51
- printer_apis: {
29
+ epson: {
52
30
  type: 'object',
53
31
  properties: {
54
- epson: {
55
- type: 'object',
56
- properties: {
57
- enabled: {
58
- type: 'boolean',
59
- default: true
60
- },
61
- pre_connect: oneOf({
62
- description: 'Try connecting to the printer without an actual printing task',
63
- type: 'array',
64
- items: {
65
- description: 'Views or conditions where a printer connection is tried just in case',
66
- type: 'string',
67
- enum: [
68
- 'payment_view'
69
- ]
70
- }
71
- }),
72
- allow_force_narrow: oneOf({
73
- description: 'Potentially allow forcing 60mm-print on 80mm-printers via UI toggle ',
74
- type: 'boolean',
75
- default: false
76
- })
77
- }
32
+ enabled: {
33
+ type: 'boolean',
34
+ default: true
78
35
  },
79
- epson_fp_it: {
80
- type: 'object',
81
- properties: {
82
- enabled: {
83
- type: 'boolean',
84
- default: false
85
- }
36
+ background_release_seconds: {
37
+ description: 'The time in seconds that the scanner will stay connected when the app is send to the background (for other apps to grab afterwards)',
38
+ type: 'integer',
39
+ example: 15,
40
+ minimum: 0,
41
+ maximum: 30, // to be on the safe side of Apple
42
+ default: 25
43
+ }
44
+ }
45
+ }
46
+ }
47
+ },
48
+ printer_apis: {
49
+ type: 'object',
50
+ properties: {
51
+ epson: {
52
+ type: 'object',
53
+ properties: {
54
+ enabled: {
55
+ type: 'boolean',
56
+ default: true
57
+ },
58
+ pre_connect: oneOf({
59
+ description: 'Try connecting to the printer without an actual printing task',
60
+ type: 'array',
61
+ items: {
62
+ description: 'Views or conditions where a printer connection is tried just in case',
63
+ type: 'string',
64
+ enum: [
65
+ 'payment_view'
66
+ ]
86
67
  }
68
+ }),
69
+ allow_force_narrow: oneOf({
70
+ description: 'Potentially allow forcing 60mm-print on 80mm-printers via UI toggle ',
71
+ type: 'boolean',
72
+ default: false
73
+ })
74
+ }
75
+ },
76
+ epson_fp_it: {
77
+ type: 'object',
78
+ properties: {
79
+ enabled: {
80
+ type: 'boolean',
81
+ default: false
87
82
  }
88
83
  }
84
+ }
85
+ }
86
+ },
87
+ terminal_apis: {
88
+ type: 'object',
89
+ properties: {
90
+ adyen: {
91
+ type: 'object',
92
+ properties: {
93
+ enabled: {
94
+ type: 'boolean',
95
+ default: true
96
+ },
97
+ webserver: oneOf({
98
+ description: 'Defines which webserver implementation to use for secondary cashier messages (terminal -> POS)',
99
+ type: 'string',
100
+ enum: [
101
+ 'none', // messages will not be visible on the POS but overall functionality is not affected
102
+ 'criollo', // there have been issues (BSH)
103
+ 'telegraph' // more stable than criollo
104
+ ],
105
+ default: 'telegraph'
106
+ }),
107
+ inter_call_delay_seconds: oneOf({
108
+ type: 'number',
109
+ description: 'Artificial delay between a previous terminal response and the next terminal request when performed automatically (some devices require additional cooldowns to avoid \'busy\' states)',
110
+ minimum: 0.0,
111
+ maximum: 10.0,
112
+ multipleOf: 0.001,
113
+ default: 0.0
114
+ })
115
+ }
89
116
  },
90
- terminal_apis: {
117
+ concardis: {
91
118
  type: 'object',
92
119
  properties: {
93
- adyen: {
94
- type: 'object',
95
- properties: {
96
- enabled: {
97
- type: 'boolean',
98
- default: true
99
- },
100
- webserver: oneOf({
101
- description: 'Defines which webserver implementation to use for secondary cashier messages (terminal -> POS)',
102
- type: 'string',
103
- enum: [
104
- 'none', // messages will not be visible on the POS but overall functionality is not affected
105
- 'criollo', // there have been issues (BSH)
106
- 'telegraph' // more stable than criollo
107
- ],
108
- default: 'telegraph'
109
- }),
110
- inter_call_delay_seconds: oneOf({
111
- type: 'number',
112
- description: 'Artificial delay between a previous terminal response and the next terminal request when performed automatically (some devices require additional cooldowns to avoid \'busy\' states)',
113
- minimum: 0.0,
114
- maximum: 10.0,
115
- multipleOf: 0.001,
116
- default: 0.0
117
- })
118
- }
120
+ enabled: {
121
+ type: 'boolean',
122
+ default: true
119
123
  },
120
- concardis: {
121
- type: 'object',
122
- properties: {
123
- enabled: {
124
- type: 'boolean',
125
- default: true
126
- },
127
- inter_call_delay_seconds: oneOf({
128
- type: 'number',
129
- description: 'Artificial delay between a previous terminal response and the next terminal request when performed automatically (some devices require additional cooldowns to avoid \'busy\' states)',
130
- minimum: 0.0,
131
- maximum: 10.0,
132
- multipleOf: 0.001,
133
- default: 0.0
134
- })
135
- }
124
+ inter_call_delay_seconds: oneOf({
125
+ type: 'number',
126
+ description: 'Artificial delay between a previous terminal response and the next terminal request when performed automatically (some devices require additional cooldowns to avoid \'busy\' states)',
127
+ minimum: 0.0,
128
+ maximum: 10.0,
129
+ multipleOf: 0.001,
130
+ default: 0.0
131
+ })
132
+ }
133
+ },
134
+ opi: {
135
+ type: 'object',
136
+ properties: {
137
+ enabled: {
138
+ type: 'boolean',
139
+ default: true
136
140
  },
137
- opi: {
138
- type: 'object',
139
- properties: {
140
- enabled: {
141
- type: 'boolean',
142
- default: true
143
- },
144
- swiss: oneOf({
145
- description: 'Defines if swiss-specific handling of the OPI protocol will be used',
146
- type: 'boolean',
147
- default: false
148
- }),
149
- omit_amount_on_reversals: oneOf({
150
- description: 'Defines if amount can be omitted in reversals',
151
- type: 'boolean',
152
- default: false
153
- }),
154
- inter_call_delay_seconds: oneOf({
155
- type: 'number',
156
- description: 'Artificial delay between a previous terminal response and the next terminal request when performed automatically (some devices require additional cooldowns to avoid \'busy\' states)',
157
- minimum: 0.0,
158
- maximum: 10.0,
159
- multipleOf: 0.001,
160
- default: 1.0 // defaults to 3.0 if Swiss
161
- }),
162
- language_code: oneOf({
163
- type: 'string',
164
- description: 'Language code send in card service requests. ISO 639-1:2002.',
165
- example: 'de',
166
- minLength: 2,
167
- maxLength: 2,
168
- default: null // fallback is defined by the current POS language
169
- })
170
- }
141
+ swiss: oneOf({
142
+ description: 'Defines if swiss-specific handling of the OPI protocol will be used',
143
+ type: 'boolean',
144
+ default: false
145
+ }),
146
+ omit_amount_on_reversals: oneOf({
147
+ description: 'Defines if amount can be omitted in reversals',
148
+ type: 'boolean',
149
+ default: false
150
+ }),
151
+ inter_call_delay_seconds: oneOf({
152
+ type: 'number',
153
+ description: 'Artificial delay between a previous terminal response and the next terminal request when performed automatically (some devices require additional cooldowns to avoid \'busy\' states)',
154
+ minimum: 0.0,
155
+ maximum: 10.0,
156
+ multipleOf: 0.001,
157
+ default: 1.0 // defaults to 3.0 if Swiss
158
+ }),
159
+ language_code: oneOf({
160
+ type: 'string',
161
+ description: 'Language code send in card service requests. ISO 639-1:2002.',
162
+ example: 'de',
163
+ minLength: 2,
164
+ maxLength: 2,
165
+ default: null // fallback is defined by the current POS language
166
+ })
167
+ }
168
+ },
169
+ sumup: {
170
+ type: 'object',
171
+ properties: {
172
+ enabled: {
173
+ type: 'boolean',
174
+ default: true
171
175
  },
172
- sumup: {
173
- type: 'object',
174
- properties: {
175
- enabled: {
176
- type: 'boolean',
177
- default: true
178
- },
179
- inter_call_delay_seconds: oneOf({
180
- type: 'number',
181
- description: 'Artificial delay between a previous terminal response and the next terminal request when performed automatically (some devices require additional cooldowns to avoid \'busy\' states)',
182
- minimum: 0.0,
183
- maximum: 10.0,
184
- multipleOf: 0.001,
185
- default: 0.0
186
- })
187
- }
176
+ inter_call_delay_seconds: oneOf({
177
+ type: 'number',
178
+ description: 'Artificial delay between a previous terminal response and the next terminal request when performed automatically (some devices require additional cooldowns to avoid \'busy\' states)',
179
+ minimum: 0.0,
180
+ maximum: 10.0,
181
+ multipleOf: 0.001,
182
+ default: 0.0
183
+ })
184
+ }
185
+ },
186
+ tim: {
187
+ type: 'object',
188
+ properties: {
189
+ enabled: {
190
+ type: 'boolean',
191
+ default: true
188
192
  },
189
- tim: {
190
- type: 'object',
191
- properties: {
192
- enabled: {
193
- type: 'boolean',
194
- default: true
195
- },
196
- inter_call_delay_seconds: oneOf({
197
- type: 'number',
198
- description: 'Artificial delay between a previous terminal response and the next terminal request when performed automatically (some devices require additional cooldowns to avoid \'busy\' states)',
199
- minimum: 0.0,
200
- maximum: 10.0,
201
- multipleOf: 0.001,
202
- default: 0.0
203
- })
204
- }
193
+ inter_call_delay_seconds: oneOf({
194
+ type: 'number',
195
+ description: 'Artificial delay between a previous terminal response and the next terminal request when performed automatically (some devices require additional cooldowns to avoid \'busy\' states)',
196
+ minimum: 0.0,
197
+ maximum: 10.0,
198
+ multipleOf: 0.001,
199
+ default: 0.0
200
+ })
201
+ }
202
+ },
203
+ spos: {
204
+ type: 'object',
205
+ properties: {
206
+ enabled: {
207
+ type: 'boolean',
208
+ default: true
205
209
  },
206
- spos: {
207
- type: 'object',
208
- properties: {
209
- enabled: {
210
- type: 'boolean',
211
- default: true
212
- },
213
- inter_call_delay_seconds: oneOf({
214
- type: 'number',
215
- description: 'Artificial delay between a previous terminal response and the next terminal request when performed automatically (some devices require additional cooldowns to avoid \'busy\' states)',
216
- minimum: 0.0,
217
- maximum: 10.0,
218
- multipleOf: 0.001,
219
- default: 0.0
220
- })
221
- }
210
+ inter_call_delay_seconds: oneOf({
211
+ type: 'number',
212
+ description: 'Artificial delay between a previous terminal response and the next terminal request when performed automatically (some devices require additional cooldowns to avoid \'busy\' states)',
213
+ minimum: 0.0,
214
+ maximum: 10.0,
215
+ multipleOf: 0.001,
216
+ default: 0.0
217
+ })
218
+ }
219
+ },
220
+ softpay: {
221
+ type: 'object',
222
+ properties: {
223
+ enabled: {
224
+ type: 'boolean',
225
+ default: true
222
226
  },
223
- softpay: {
224
- type: 'object',
225
- properties: {
226
- enabled: {
227
- type: 'boolean',
228
- default: true
229
- },
230
- inter_call_delay_seconds: oneOf({
231
- type: 'number',
232
- description: 'Artificial delay between a previous terminal response and the next terminal request when performed automatically (some devices require additional cooldowns to avoid \'busy\' states)',
233
- minimum: 0.0,
234
- maximum: 10.0,
235
- multipleOf: 0.001,
236
- default: 0.0
237
- })
238
- }
227
+ inter_call_delay_seconds: oneOf({
228
+ type: 'number',
229
+ description: 'Artificial delay between a previous terminal response and the next terminal request when performed automatically (some devices require additional cooldowns to avoid \'busy\' states)',
230
+ minimum: 0.0,
231
+ maximum: 10.0,
232
+ multipleOf: 0.001,
233
+ default: 0.0
234
+ })
235
+ }
236
+ },
237
+ ZVT: { // capitalized for historical reasons
238
+ type: 'object',
239
+ properties: {
240
+ enabled: {
241
+ type: 'boolean',
242
+ default: true
239
243
  },
240
- ZVT: { // capitalized for historical reasons
241
- type: 'object',
242
- properties: {
243
- enabled: {
244
- type: 'boolean',
245
- default: true
246
- },
247
- inter_call_delay_seconds: oneOf({
248
- type: 'number',
249
- description: 'Artificial delay between a previous terminal response and the next terminal request when performed automatically (some devices require additional cooldowns to avoid \'busy\' states)',
250
- minimum: 0.0,
251
- maximum: 10.0,
252
- multipleOf: 0.001,
253
- default: 0.0
254
- })
255
- }
256
- }
244
+ inter_call_delay_seconds: oneOf({
245
+ type: 'number',
246
+ description: 'Artificial delay between a previous terminal response and the next terminal request when performed automatically (some devices require additional cooldowns to avoid \'busy\' states)',
247
+ minimum: 0.0,
248
+ maximum: 10.0,
249
+ multipleOf: 0.001,
250
+ default: 0.0
251
+ })
257
252
  }
258
253
  }
259
254
  }
260
255
  }
261
- ]
256
+ }
262
257
  }
@@ -148,6 +148,11 @@ const cartReceipt = {
148
148
  type: 'boolean',
149
149
  default: true
150
150
  }),
151
+ print_barcode: oneOf({
152
+ description: 'Defines if the scanned barcode of a press item is printed on the specific receipt, as its own line below the product name. It governs press lines booked against a titled article, whose own name already identifies them. A line booked against one of the generic press fallback articles configured in features.press_item_barcode.products prints its barcode regardless of this flag, because the barcode is the only thing that tells two such lines apart. Defaults to true like print_serial_number, and is inert until that prefix map is populated. Per-receipt-type defaults are applied by the client, since this object is shared by every cart receipt type.',
153
+ type: 'boolean',
154
+ default: true
155
+ }),
151
156
  print_service_question_answers: oneOf({
152
157
  description: 'States if the product customization answers and questions will be printed',
153
158
  type: 'boolean',
@@ -162,12 +162,12 @@ module.exports = oneOf({
162
162
  }
163
163
  },
164
164
  opening_hours_exceptions: {
165
- description: 'Additive Open or Closed exceptions over a date range, optionally limited to selected weekdays. Closed is full day only. 0 = Monday … 6 = Sunday.',
165
+ description: 'Additive Open or Closed exceptions over a date range, optionally limited to selected weekdays. Closed is full day only. 0 = Monday … 6 = Sunday. `from` and `to` are required keys on every item: omit neither. Open all-day is `from: null` and `to: null` (both present). Open specific hours requires both as HH:mm. Closed requires `from: null` and `to: null`.',
166
166
  type: 'array',
167
167
  items: {
168
168
  type: 'object',
169
169
  additionalProperties: false,
170
- required: ['type', 'start_date', 'end_date'],
170
+ required: ['type', 'start_date', 'end_date', 'from', 'to'],
171
171
  properties: {
172
172
  type: {
173
173
  type: 'string',
@@ -190,12 +190,14 @@ module.exports = oneOf({
190
190
  }
191
191
  },
192
192
  from: {
193
+ description: 'Required. Open all-day: must be null together with `to` (the key must still be present). Open specific hours: HH:mm start, with `to` also set. Closed: always null.',
193
194
  oneOf: [
194
195
  { type: 'null' },
195
196
  { type: 'string', pattern: '^([01]\\d|2[0-3]):[0-5]\\d$' }
196
197
  ]
197
198
  },
198
199
  to: {
200
+ description: 'Required. Open all-day: must be null together with `from` (the key must still be present). Open specific hours: HH:mm end, with `from` also set and earlier than `to`. Closed: always null. Omitting this key is invalid and is not treated as all-day.',
199
201
  oneOf: [
200
202
  { type: 'null' },
201
203
  { type: 'string', pattern: '^([01]\\d|2[0-3]):[0-5]\\d$' }
@@ -226,6 +228,7 @@ module.exports = oneOf({
226
228
  required: ['type']
227
229
  },
228
230
  then: {
231
+ required: ['from', 'to'],
229
232
  properties: {
230
233
  from: { type: 'null' },
231
234
  to: { type: 'null' },
@@ -244,12 +247,15 @@ module.exports = oneOf({
244
247
  then: {
245
248
  oneOf: [
246
249
  {
250
+ description: 'Open all day: from and to must both be present and null (00:00–23:59). A break may sit strictly inside that window. Omitting either key is not all-day.',
251
+ required: ['from', 'to'],
247
252
  properties: {
248
253
  from: { type: 'null' },
249
254
  to: { type: 'null' }
250
255
  }
251
256
  },
252
257
  {
258
+ description: 'Open specific hours: from and to must both be present as HH:mm.',
253
259
  properties: {
254
260
  from: { type: 'string' },
255
261
  to: { type: 'string' }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tillhub/schemas",
3
- "version": "6.455.0",
3
+ "version": "6.457.0",
4
4
  "private": false,
5
5
  "description": "All Tillhub API OpenAPI 3 compatible JSON Schemas.",
6
6
  "main": "./lib/index.js",
@@ -17,7 +17,8 @@
17
17
  "clean": "rm -rf coverage coverage.lcov .nyc_output",
18
18
  "deploy:static": "firebase deploy --only=hosting",
19
19
  "compile": "node compile",
20
- "new:schema": "hygen schema with-prompt"
20
+ "new:schema": "hygen schema with-prompt",
21
+ "commitlint": "commitlint --from origin/master --to HEAD --verbose"
21
22
  },
22
23
  "repository": {
23
24
  "type": "git",
@@ -51,6 +52,8 @@
51
52
  ],
52
53
  "homepage": "https://github.com/tillhub/schemas#readme",
53
54
  "devDependencies": {
55
+ "@commitlint/cli": "^19.8.1",
56
+ "@commitlint/config-conventional": "^19.8.1",
54
57
  "@semantic-release/changelog": "^6.0.3",
55
58
  "@semantic-release/exec": "^7.1.0",
56
59
  "@semantic-release/git": "^10.0.1",
@@ -47,6 +47,16 @@ test('Configurations: opening_hours_exceptions accepts Open all-day, Open hours,
47
47
  to: null,
48
48
  reason: 'Sunday brunch all day'
49
49
  },
50
+ {
51
+ type: 'open',
52
+ start_date: '2026-12-21',
53
+ end_date: '2026-12-21',
54
+ from: null,
55
+ to: null,
56
+ break_from: '15:00',
57
+ break_to: '16:00',
58
+ reason: 'All day with afternoon break'
59
+ },
50
60
  {
51
61
  type: 'open',
52
62
  start_date: '2026-07-02',
@@ -93,6 +103,40 @@ test('Configurations: opening_hours_exceptions rejects Closed with specific hour
93
103
  t.end()
94
104
  })
95
105
 
106
+ test('Configurations: opening_hours_exceptions rejects omitted Open from/to keys', function (t) {
107
+ t.plan(1)
108
+ const valid = validateUpdatePayload({
109
+ reservations: {
110
+ opening_hours_exceptions: [
111
+ {
112
+ type: 'open',
113
+ start_date: '2026-12-20',
114
+ end_date: '2026-12-20'
115
+ }
116
+ ]
117
+ }
118
+ })
119
+ t.notOk(valid, 'Open from/to keys are required; omitted keys are not all-day')
120
+ t.end()
121
+ })
122
+
123
+ test('Configurations: opening_hours_exceptions rejects omitted Closed from/to keys', function (t) {
124
+ t.plan(1)
125
+ const valid = validateUpdatePayload({
126
+ reservations: {
127
+ opening_hours_exceptions: [
128
+ {
129
+ type: 'closed',
130
+ start_date: '2026-01-01',
131
+ end_date: '2026-01-01'
132
+ }
133
+ ]
134
+ }
135
+ })
136
+ t.notOk(valid, 'Closed from/to keys are required and must be null')
137
+ t.end()
138
+ })
139
+
96
140
  test('Configurations: opening_hours_exceptions rejects mixed Open from/to', function (t) {
97
141
  t.plan(1)
98
142
  const valid = validateUpdatePayload({
@@ -141,3 +185,156 @@ test('Configurations: payloads without opening_hours_exceptions remain valid', f
141
185
  t.ok(valid, ajv.errorsText(validateUpdatePayload.errors))
142
186
  t.end()
143
187
  })
188
+
189
+ test('Configurations: interfaces seeds scanner_apis.zebra.enabled when scanner_apis is absent', function (t) {
190
+ t.plan(2)
191
+ const ajvWithDefaults = new Ajv({ useDefaults: true })
192
+ const validateCreatePayload = ajvWithDefaults.compile(require('../../lib/v1/configurations').create.request)
193
+ const payload = {
194
+ name: 'Default Configurations',
195
+ owner: 'self',
196
+ interfaces: {
197
+ terminal_apis: {
198
+ opi: { enabled: true },
199
+ tim: { enabled: true },
200
+ adyen: { enabled: true },
201
+ sumup: { enabled: true },
202
+ concardis: { enabled: true }
203
+ }
204
+ }
205
+ }
206
+ const valid = validateCreatePayload(payload)
207
+
208
+ t.ok(valid, ajvWithDefaults.errorsText(validateCreatePayload.errors))
209
+ t.deepEquals(payload.interfaces.scanner_apis, {
210
+ zebra: {
211
+ enabled: true,
212
+ background_release_seconds: 25
213
+ }
214
+ }, 'zebra is enabled by default')
215
+ t.end()
216
+ })
217
+
218
+ test('Configurations: interfaces keeps an explicitly disabled zebra scanner', function (t) {
219
+ t.plan(2)
220
+ const ajvWithDefaults = new Ajv({ useDefaults: true })
221
+ const validateCreatePayload = ajvWithDefaults.compile(require('../../lib/v1/configurations').create.request)
222
+ const payload = {
223
+ name: 'Default Configurations',
224
+ owner: 'self',
225
+ interfaces: {
226
+ scanner_apis: {
227
+ zebra: { enabled: false }
228
+ }
229
+ }
230
+ }
231
+ const valid = validateCreatePayload(payload)
232
+
233
+ t.ok(valid, ajvWithDefaults.errorsText(validateCreatePayload.errors))
234
+ t.notOk(payload.interfaces.scanner_apis.zebra.enabled, 'explicit value wins over the default')
235
+ t.end()
236
+ })
237
+
238
+ test('Configurations: interfaces still accepts null', function (t) {
239
+ t.plan(2)
240
+ const validCreate = ajv.compile(require('../../lib/v1/configurations').create.request)({
241
+ name: 'Default Configurations',
242
+ owner: 'self',
243
+ interfaces: null
244
+ })
245
+ t.ok(validCreate, 'null interfaces is valid on create')
246
+
247
+ const validUpdate = validateUpdatePayload({ interfaces: null })
248
+ t.ok(validUpdate, ajv.errorsText(validateUpdatePayload.errors))
249
+ t.end()
250
+ })
251
+
252
+ const pressUuid = '5b1b1b1b-1b1b-4b1b-8b1b-1b1b1b1b1b1b'
253
+
254
+ test('Configurations: press_item_barcode accepts the documented contract', function (t) {
255
+ t.plan(1)
256
+ const valid = validateUpdatePayload({
257
+ features: {
258
+ press_item_barcode: {
259
+ enabled: false,
260
+ lookup_priority: 'catalog_first',
261
+ price_source_on_catalog_hit: 'barcode',
262
+ products: {
263
+ 414: pressUuid,
264
+ 419: pressUuid,
265
+ 434: pressUuid,
266
+ 439: pressUuid
267
+ }
268
+ }
269
+ }
270
+ })
271
+ t.ok(valid, ajv.errorsText(validateUpdatePayload.errors))
272
+ t.end()
273
+ })
274
+
275
+ test('Configurations: press_item_barcode accepts a prefix the client does not know', function (t) {
276
+ t.plan(1)
277
+ const valid = validateUpdatePayload({
278
+ features: {
279
+ press_item_barcode: { enabled: true, products: { 444: pressUuid } }
280
+ }
281
+ })
282
+ t.ok(valid, 'the supported prefixes are the map keys, so a new one needs no schema change')
283
+ t.end()
284
+ })
285
+
286
+ test('Configurations: press_item_barcode accepts barcode_first with a catalog-hit price source', function (t) {
287
+ t.plan(1)
288
+ const valid = validateUpdatePayload({
289
+ features: {
290
+ press_item_barcode: {
291
+ lookup_priority: 'barcode_first',
292
+ price_source_on_catalog_hit: 'catalog'
293
+ }
294
+ }
295
+ })
296
+ t.ok(valid, 'the combination is inert rather than invalid and must not be rejected')
297
+ t.end()
298
+ })
299
+
300
+ test('Configurations: press_item_barcode accepts an empty products map', function (t) {
301
+ t.plan(1)
302
+ const valid = validateUpdatePayload({
303
+ features: {
304
+ press_item_barcode: { enabled: true, products: {} }
305
+ }
306
+ })
307
+ t.ok(valid, 'equivalent to disabled, which the client resolves rather than the schema')
308
+ t.end()
309
+ })
310
+
311
+ test('Configurations: press_item_barcode rejects a prefix that is not exactly three digits', function (t) {
312
+ t.plan(2)
313
+ t.notOk(validateUpdatePayload({
314
+ features: { press_item_barcode: { products: { 41: pressUuid } } }
315
+ }), 'two digits')
316
+ t.notOk(validateUpdatePayload({
317
+ features: { press_item_barcode: { products: { 4141: pressUuid } } }
318
+ }), 'four digits')
319
+ t.end()
320
+ })
321
+
322
+ test('Configurations: press_item_barcode rejects a product value that is not a uuid', function (t) {
323
+ t.plan(1)
324
+ const valid = validateUpdatePayload({
325
+ features: { press_item_barcode: { products: { 414: 'presse-19' } } }
326
+ })
327
+ t.notOk(valid, 'the map holds article uuids, not names or numbers')
328
+ t.end()
329
+ })
330
+
331
+ test('Configurations: press_item_barcode rejects unknown enum values', function (t) {
332
+ t.plan(2)
333
+ t.notOk(validateUpdatePayload({
334
+ features: { press_item_barcode: { lookup_priority: 'catalogue_first' } }
335
+ }), 'lookup_priority')
336
+ t.notOk(validateUpdatePayload({
337
+ features: { press_item_barcode: { price_source_on_catalog_hit: 'ean' } }
338
+ }), 'price_source_on_catalog_hit')
339
+ t.end()
340
+ })