Summary|
Tags|
Feature:
api/reservation-advanced-lifecycle/reservation-advanced-lifecycle.feature|
Ticketing MVP - Reservation Advanced Lifecycle
As a QA engineer
I want to validate advanced reservation lifecycle scenarios
So that expiration, concurrency, and business rules are properly enforced
Scenario: [1:13]
Scenario 1 - Pure Expiration Without Payment (inventory released)
ms: 2250
>>
Background:
7
* def baseUrlEvents = karate.properties['baseUrlEvents'] || 'http://localhost:8081'
0
22:53:18.777 karate.env system property was: null
8
* def baseUrlTicketing = karate.properties['baseUrlTicketing'] || 'http://localhost:8082'
0
9
* def UUID = Java.type('java.util.UUID')
0
10
* def eventTitle = 'Advanced Lifecycle Test ' + UUID.randomUUID() + ''
0
11
* def futureDate = '2026-12-15T20:00:00'
0
# Setup: Create room
16
Given url baseUrlEvents + '/api/v1/rooms'
0
17
And header X-Role = 'ADMIN'
0
18
And header X-User-Id = '00000000-0000-0000-0000-000000000001'
0
19
And request
0
{
"name": "Pure Expiration Test Room",
"maxCapacity": 100
}
26
When method post
10
22:53:18.779 request:
1 > POST http://localhost:8081/api/v1/rooms
1 > X-Role: ADMIN
1 > X-User-Id: 00000000-0000-0000-0000-000000000001
1 > Content-Type: application/json; charset=UTF-8
1 > Content-Length: 54
1 > Host: localhost:8081
1 > Connection: Keep-Alive
1 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
1 > Accept-Encoding: gzip,deflate
{"name":"Pure Expiration Test Room","maxCapacity":100}
22:53:18.788 response time in milliseconds: 9
1 < 201
1 < Content-Type: application/json
1 < Transfer-Encoding: chunked
1 < Date: Wed, 08 Apr 2026 03:53:18 GMT
1 < Keep-Alive: timeout=60
1 < Connection: keep-alive
{"id":"e34c88dc-faaa-4ca2-bf19-473e5beaabf9","name":"Pure Expiration Test Room","maxCapacity":100,"created_at":"2026-04-08T03:53:18.78164586","updated_at":"2026-04-08T03:53:18.781658495"}
27
Then status 201
0
28
* def roomId = response.id ? response.id : response.roomId
0
29
* print 'Room created:', roomId
0
22:53:18.789 [print] Room created: e34c88dc-faaa-4ca2-bf19-473e5beaabf9
# Setup: Create event
32
* def scenarioTitle = eventTitle + ' - Pure Expiration'
0
33
Given url baseUrlEvents + '/api/v1/events'
0
34
And header X-Role = 'ADMIN'
0
35
And header X-User-Id = '00000000-0000-0000-0000-000000000001'
0
36
And request
0
{
"roomId": "#(roomId)",
"title": "#(scenarioTitle)",
"description": "Test pure expiration without payment",
"date": "#(futureDate)",
"capacity": 10,
"enableSeats": false
}
47
When method post
12
22:53:18.790 request:
2 > POST http://localhost:8081/api/v1/events
2 > X-Role: ADMIN
2 > X-User-Id: 00000000-0000-0000-0000-000000000001
2 > Content-Type: application/json; charset=UTF-8
2 > Content-Length: 254
2 > Host: localhost:8081
2 > Connection: Keep-Alive
2 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
2 > Accept-Encoding: gzip,deflate
{"roomId":"e34c88dc-faaa-4ca2-bf19-473e5beaabf9","title":"Advanced Lifecycle Test 6fe82fc3-6140-4fd4-9549-d40687e2ab38 - Pure Expiration","description":"Test pure expiration without payment","date":"2026-12-15T20:00:00","capacity":10,"enableSeats":false}
22:53:18.801 response time in milliseconds: 11
2 < 201
2 < Content-Type: application/json
2 < Transfer-Encoding: chunked
2 < Date: Wed, 08 Apr 2026 03:53:18 GMT
2 < Keep-Alive: timeout=60
2 < Connection: keep-alive
{"id":"571e2a05-0a31-4dd1-a274-7b69305d0d4e","roomId":"e34c88dc-faaa-4ca2-bf19-473e5beaabf9","title":"Advanced Lifecycle Test 6fe82fc3-6140-4fd4-9549-d40687e2ab38 - Pure Expiration","description":"Test pure expiration without payment","date":"2026-12-15T20:00:00","capacity":10,"status":"DRAFT","createdAt":"2026-04-08T03:53:18.794679316","updatedAt":"2026-04-08T03:53:18.794687761","createdBy":"00000000-0000-0000-0000-000000000001","imageUrl":null,"subtitle":null,"location":null,"director":null,"castMembers":null,"duration":null,"tag":null,"isLimited":false,"isFeatured":false,"enableSeats":false,"author":null}
48
Then status 201
0
49
* def eventId = response.id ? response.id : response.eventId
0
# Setup: Create tier
52
Given url baseUrlEvents + '/api/v1/events/' + eventId + '/tiers'
0
53
And header X-Role = 'ADMIN'
0
54
And request
0
[
{
"tierType": "GENERAL",
"price": 50.00,
"quota": 10
}
]
64
When method post
13
22:53:18.802 request:
3 > POST http://localhost:8081/api/v1/events/571e2a05-0a31-4dd1-a274-7b69305d0d4e/tiers
3 > X-Role: ADMIN
3 > Content-Type: application/json; charset=UTF-8
3 > Content-Length: 48
3 > Host: localhost:8081
3 > Connection: Keep-Alive
3 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
3 > Accept-Encoding: gzip,deflate
[{"tierType":"GENERAL","price":50.0,"quota":10}]
22:53:18.815 response time in milliseconds: 12
3 < 201
3 < Content-Type: application/json
3 < Transfer-Encoding: chunked
3 < Date: Wed, 08 Apr 2026 03:53:18 GMT
3 < Keep-Alive: timeout=60
3 < Connection: keep-alive
{"eventId":"571e2a05-0a31-4dd1-a274-7b69305d0d4e","tiers":[{"id":"2eb079a0-080c-420a-aabe-e92b4ef735a2","tierType":"GENERAL","price":50.0,"quota":10,"validFrom":null,"validUntil":null,"createdAt":"2026-04-08T03:53:18.808269153","updatedAt":"2026-04-08T03:53:18.808288407"}]}
65
Then status 201
0
66
* def tierId = response.tiers[0].id || response.tiers[0].tierId || response[0].id || response[0].tierId
0
67
* print 'Tier created with quota=10:', tierId
0
22:53:18.815 [print] Tier created with quota=10: 2eb079a0-080c-420a-aabe-e92b4ef735a2
# Setup: Publish event
70
Given url baseUrlEvents + '/api/v1/events/' + eventId + '/publish'
0
71
And header X-Role = 'ADMIN'
0
72
When method patch
11
22:53:18.816 request:
4 > PATCH http://localhost:8081/api/v1/events/571e2a05-0a31-4dd1-a274-7b69305d0d4e/publish
4 > X-Role: ADMIN
4 > Host: localhost:8081
4 > Connection: Keep-Alive
4 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
4 > Accept-Encoding: gzip,deflate
22:53:18.826 response time in milliseconds: 10
4 < 200
4 < Content-Type: application/json
4 < Transfer-Encoding: chunked
4 < Date: Wed, 08 Apr 2026 03:53:18 GMT
4 < Keep-Alive: timeout=60
4 < Connection: keep-alive
{"id":"571e2a05-0a31-4dd1-a274-7b69305d0d4e","roomId":"e34c88dc-faaa-4ca2-bf19-473e5beaabf9","title":"Advanced Lifecycle Test 6fe82fc3-6140-4fd4-9549-d40687e2ab38 - Pure Expiration","description":"Test pure expiration without payment","date":"2026-12-15T20:00:00","capacity":10,"status":"PUBLISHED","createdAt":"2026-04-08T03:53:18.794679","updatedAt":"2026-04-08T03:53:18.820115955","createdBy":"00000000-0000-0000-0000-000000000001","imageUrl":null,"subtitle":null,"location":null,"director":null,"castMembers":null,"duration":null,"tag":null,"isLimited":false,"isFeatured":false,"enableSeats":false,"author":null}
73
Then status 200
0
74
* print 'Event published'
0
22:53:18.827 [print] Event published
# HU-05: Create reservation (quota now 9 available)
77
* def buyerId1 = UUID.randomUUID() + ''
0
78
* def buyerEmail1 = 'buyer1-' + buyerId1.substring(0,8) + '@karate-test.com'
11
79
Given url baseUrlTicketing + '/api/v1/reservations'
0
80
And header X-User-Id = buyerId1
0
81
And request
0
{
"eventId": "#(eventId)",
"tierId": "#(tierId)",
"buyerEmail": "#(buyerEmail1)"
}
89
When method post
27
22:53:18.839 request:
5 > POST http://localhost:8082/api/v1/reservations
5 > X-User-Id: 9c7a0e50-0ed8-4607-9ada-50c6cb1cd5cf
5 > Content-Type: application/json; charset=UTF-8
5 > Content-Length: 145
5 > Host: localhost:8082
5 > Connection: Keep-Alive
5 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
5 > Accept-Encoding: gzip,deflate
{"eventId":"571e2a05-0a31-4dd1-a274-7b69305d0d4e","tierId":"2eb079a0-080c-420a-aabe-e92b4ef735a2","buyerEmail":"buyer1-9c7a0e50@karate-test.com"}
22:53:18.865 response time in milliseconds: 26
5 < 201
5 < Content-Type: application/json
5 < Transfer-Encoding: chunked
5 < Date: Wed, 08 Apr 2026 03:53:18 GMT
5 < Keep-Alive: timeout=60
5 < Connection: keep-alive
{"id":"a0edc81d-8a02-4ab1-bda8-afff9f849d1d","eventId":"571e2a05-0a31-4dd1-a274-7b69305d0d4e","tierId":"2eb079a0-080c-420a-aabe-e92b4ef735a2","buyerId":"9c7a0e50-0ed8-4607-9ada-50c6cb1cd5cf","status":"PENDING","createdAt":"2026-04-08T03:53:18.858977096","updatedAt":"2026-04-08T03:53:18.858977096","validUntilAt":"2026-04-08T04:03:18.858977096"}
90
Then status 201
0
91
* def reservationId1 = response.id
0
92
* print 'Reservation 1 created (status=PENDING):', reservationId1
0
22:53:18.866 [print] Reservation 1 created (status=PENDING): a0edc81d-8a02-4ab1-bda8-afff9f849d1d
# Verify tier quota is reduced (should be 9 now: 10 - 1)
95
Given url baseUrlEvents + '/api/v1/events/' + eventId + '/tiers'
0
96
When method get
6
22:53:18.867 request:
6 > GET http://localhost:8081/api/v1/events/571e2a05-0a31-4dd1-a274-7b69305d0d4e/tiers
6 > Host: localhost:8081
6 > Connection: Keep-Alive
6 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
6 > Accept-Encoding: gzip,deflate
22:53:18.872 response time in milliseconds: 5
6 < 200
6 < Content-Type: application/json
6 < Transfer-Encoding: chunked
6 < Date: Wed, 08 Apr 2026 03:53:18 GMT
6 < Keep-Alive: timeout=60
6 < Connection: keep-alive
{"eventId":"571e2a05-0a31-4dd1-a274-7b69305d0d4e","tiers":[{"id":"2eb079a0-080c-420a-aabe-e92b4ef735a2","tierType":"GENERAL","price":50.00,"quota":9,"validFrom":null,"validUntil":null,"createdAt":"2026-04-08T03:53:18.808269","updatedAt":"2026-04-08T03:53:18.851518"}]}
97
Then status 200
0
98
* def tierAfterRes = response.tiers[0]
0
99
* print 'After reservation 1: quota=' + tierAfterRes.quota
0
22:53:18.873 [print] After reservation 1: quota=9
100
* assert tierAfterRes.quota == 9
0
# Force expiration smoothly via Time Travel API
103
* print 'Forzando expiración limpia via Time Travel...'
0
22:53:18.873 [print] Forzando expiración limpia via Time Travel...
104
Given url baseUrlTicketing + '/api/v1/testability/clock/advance'
0
105
And param minutes = 15
0
106
When method post
7
22:53:18.874 request:
7 > POST http://localhost:8082/api/v1/testability/clock/advance?minutes=15
7 > Host: localhost:8082
7 > Connection: Keep-Alive
7 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
7 > Accept-Encoding: gzip,deflate
22:53:18.880 response time in milliseconds: 6
7 < 200
7 < Content-Type: application/json
7 < Transfer-Encoding: chunked
7 < Date: Wed, 08 Apr 2026 03:53:18 GMT
7 < Keep-Alive: timeout=60
7 < Connection: keep-alive
{"status":"advanced","current_time":"2026-04-08T04:08:18.877356719"}
107
Then status 200
0
109
Given url baseUrlTicketing + '/api/v1/testability/jobs/expiration/trigger'
0
110
When method post
110
22:53:18.882 request:
8 > POST http://localhost:8082/api/v1/testability/jobs/expiration/trigger
8 > Host: localhost:8082
8 > Connection: Keep-Alive
8 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
8 > Accept-Encoding: gzip,deflate
22:53:18.991 response time in milliseconds: 109
8 < 200
8 < Content-Type: application/json
8 < Transfer-Encoding: chunked
8 < Date: Wed, 08 Apr 2026 03:53:18 GMT
8 < Keep-Alive: timeout=60
8 < Connection: keep-alive
{"status":"Triggered processExpiredBatch"}
111
Then status 200
0
112
* print 'CRONJOB de expiración desencadenado exitosamente!'
0
22:53:18.992 [print] CRONJOB de expiración desencadenado exitosamente!
# Verify reservation status changed to EXPIRED via API
115
Given url baseUrlTicketing + '/api/v1/reservations/' + reservationId1
0
116
And header X-User-Id = buyerId1
0
117
When method get
10
22:53:18.993 request:
9 > GET http://localhost:8082/api/v1/reservations/a0edc81d-8a02-4ab1-bda8-afff9f849d1d
9 > X-User-Id: 9c7a0e50-0ed8-4607-9ada-50c6cb1cd5cf
9 > Host: localhost:8082
9 > Connection: Keep-Alive
9 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
9 > Accept-Encoding: gzip,deflate
22:53:19.002 response time in milliseconds: 8
9 < 200
9 < Content-Type: application/json
9 < Transfer-Encoding: chunked
9 < Date: Wed, 08 Apr 2026 03:53:18 GMT
9 < Keep-Alive: timeout=60
9 < Connection: keep-alive
{"id":"a0edc81d-8a02-4ab1-bda8-afff9f849d1d","eventId":"571e2a05-0a31-4dd1-a274-7b69305d0d4e","tierId":"2eb079a0-080c-420a-aabe-e92b4ef735a2","status":"EXPIRED","createdAt":"2026-04-08T03:53:18.858977","updatedAt":"2026-04-08T03:53:18.983032","validUntilAt":"2026-04-08T04:03:18.858977"}
118
Then status 200
0
119
* match response.status == 'EXPIRED'
0
120
* print 'Reservation 1 confirmed expired via API'
0
22:53:19.003 [print] Reservation 1 confirmed expired via API
# Verify tier quota is restored (should be back to 10: quota released)
123
* java.lang.Thread.sleep(2000)
2001
124
Given url baseUrlEvents + '/api/v1/events/' + eventId + '/tiers'
1
125
When method get
15
22:53:21.008 request:
10 > GET http://localhost:8081/api/v1/events/571e2a05-0a31-4dd1-a274-7b69305d0d4e/tiers
10 > Host: localhost:8081
10 > Connection: Keep-Alive
10 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
10 > Accept-Encoding: gzip,deflate
22:53:21.019 response time in milliseconds: 11
10 < 200
10 < Content-Type: application/json
10 < Transfer-Encoding: chunked
10 < Date: Wed, 08 Apr 2026 03:53:21 GMT
10 < Keep-Alive: timeout=60
10 < Connection: keep-alive
{"eventId":"571e2a05-0a31-4dd1-a274-7b69305d0d4e","tiers":[{"id":"2eb079a0-080c-420a-aabe-e92b4ef735a2","tierType":"GENERAL","price":50.00,"quota":10,"validFrom":null,"validUntil":null,"createdAt":"2026-04-08T03:53:18.808269","updatedAt":"2026-04-08T03:53:18.969149"}]}
126
Then status 200
0
127
* def tierAfterExpiration = response.tiers[0]
0
128
* print 'After expiration: quota=' + tierAfterExpiration.quota
1
22:53:21.021 [print] After expiration: quota=10
129
* assert tierAfterExpiration.quota == 10
0
# Cleanup Clock
132
Given url baseUrlTicketing + '/api/v1/testability/clock/reset'
0
133
When method post
6
22:53:21.024 request:
11 > POST http://localhost:8082/api/v1/testability/clock/reset
11 > Host: localhost:8082
11 > Connection: Keep-Alive
11 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
11 > Accept-Encoding: gzip,deflate
22:53:21.028 response time in milliseconds: 4
11 < 200
11 < Content-Type: application/json
11 < Transfer-Encoding: chunked
11 < Date: Wed, 08 Apr 2026 03:53:21 GMT
11 < Keep-Alive: timeout=60
11 < Connection: keep-alive
{"status":"reset","current_time":"2026-04-08T03:53:21.027035722"}
134
Then status 200
0
135
* print '✅ Scenario 1 PASS: Pure expiration released quota (quota back to 10)'
1
22:53:21.029 [print] ✅ Scenario 1 PASS: Pure expiration released quota (quota back to 10)
Scenario: [2:138]
Scenario 2 - Payment Attempt on Expired Reservation (must fail)
ms: 158
>>
Background:
7
* def baseUrlEvents = karate.properties['baseUrlEvents'] || 'http://localhost:8081'
0
22:53:21.031 karate.env system property was: null
8
* def baseUrlTicketing = karate.properties['baseUrlTicketing'] || 'http://localhost:8082'
0
9
* def UUID = Java.type('java.util.UUID')
0
10
* def eventTitle = 'Advanced Lifecycle Test ' + UUID.randomUUID() + ''
0
11
* def futureDate = '2026-12-15T20:00:00'
0
# Setup: Create room
141
Given url baseUrlEvents + '/api/v1/rooms'
0
142
And header X-Role = 'ADMIN'
0
143
And header X-User-Id = '00000000-0000-0000-0000-000000000001'
0
144
And request
0
{
"name": "Expired Payment Test Room",
"maxCapacity": 100
}
151
When method post
9
22:53:21.033 request:
1 > POST http://localhost:8081/api/v1/rooms
1 > X-Role: ADMIN
1 > X-User-Id: 00000000-0000-0000-0000-000000000001
1 > Content-Type: application/json; charset=UTF-8
1 > Content-Length: 54
1 > Host: localhost:8081
1 > Connection: Keep-Alive
1 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
1 > Accept-Encoding: gzip,deflate
{"name":"Expired Payment Test Room","maxCapacity":100}
22:53:21.041 response time in milliseconds: 8
1 < 201
1 < Content-Type: application/json
1 < Transfer-Encoding: chunked
1 < Date: Wed, 08 Apr 2026 03:53:21 GMT
1 < Keep-Alive: timeout=60
1 < Connection: keep-alive
{"id":"0021d028-c502-4196-a7bf-4619d2781416","name":"Expired Payment Test Room","maxCapacity":100,"created_at":"2026-04-08T03:53:21.034939723","updated_at":"2026-04-08T03:53:21.034950095"}
152
Then status 201
0
153
* def roomId = response.id ? response.id : response.roomId
0
# Setup: Create event + tier + publish
156
* def scenarioTitle = eventTitle + ' - Expired Payment Test'
0
157
Given url baseUrlEvents + '/api/v1/events'
0
158
And header X-Role = 'ADMIN'
0
159
And header X-User-Id = '00000000-0000-0000-0000-000000000001'
0
160
And request
0
{
"roomId": "#(roomId)",
"title": "#(scenarioTitle)",
"description": "Test payment on expired reservation",
"date": "#(futureDate)",
"capacity": 20,
"enableSeats": false
}
171
When method post
13
22:53:21.042 request:
2 > POST http://localhost:8081/api/v1/events
2 > X-Role: ADMIN
2 > X-User-Id: 00000000-0000-0000-0000-000000000001
2 > Content-Type: application/json; charset=UTF-8
2 > Content-Length: 258
2 > Host: localhost:8081
2 > Connection: Keep-Alive
2 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
2 > Accept-Encoding: gzip,deflate
{"roomId":"0021d028-c502-4196-a7bf-4619d2781416","title":"Advanced Lifecycle Test 2d26a2c9-fcb8-4021-b784-7ed761ced1f4 - Expired Payment Test","description":"Test payment on expired reservation","date":"2026-12-15T20:00:00","capacity":20,"enableSeats":false}
22:53:21.054 response time in milliseconds: 12
2 < 201
2 < Content-Type: application/json
2 < Transfer-Encoding: chunked
2 < Date: Wed, 08 Apr 2026 03:53:21 GMT
2 < Keep-Alive: timeout=60
2 < Connection: keep-alive
{"id":"bdba69a9-1529-4edc-b362-e7a07d00d180","roomId":"0021d028-c502-4196-a7bf-4619d2781416","title":"Advanced Lifecycle Test 2d26a2c9-fcb8-4021-b784-7ed761ced1f4 - Expired Payment Test","description":"Test payment on expired reservation","date":"2026-12-15T20:00:00","capacity":20,"status":"DRAFT","createdAt":"2026-04-08T03:53:21.047846658","updatedAt":"2026-04-08T03:53:21.047855137","createdBy":"00000000-0000-0000-0000-000000000001","imageUrl":null,"subtitle":null,"location":null,"director":null,"castMembers":null,"duration":null,"tag":null,"isLimited":false,"isFeatured":false,"enableSeats":false,"author":null}
172
Then status 201
0
173
* def eventId = response.id ? response.id : response.eventId
0
175
Given url baseUrlEvents + '/api/v1/events/' + eventId + '/tiers'
0
176
And header X-Role = 'ADMIN'
0
177
And request
0
[
{
"tierType": "GENERAL",
"price": 60.00,
"quota": 20
}
]
187
When method post
11
22:53:21.055 request:
3 > POST http://localhost:8081/api/v1/events/bdba69a9-1529-4edc-b362-e7a07d00d180/tiers
3 > X-Role: ADMIN
3 > Content-Type: application/json; charset=UTF-8
3 > Content-Length: 48
3 > Host: localhost:8081
3 > Connection: Keep-Alive
3 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
3 > Accept-Encoding: gzip,deflate
[{"tierType":"GENERAL","price":60.0,"quota":20}]
22:53:21.065 response time in milliseconds: 10
3 < 201
3 < Content-Type: application/json
3 < Transfer-Encoding: chunked
3 < Date: Wed, 08 Apr 2026 03:53:21 GMT
3 < Keep-Alive: timeout=60
3 < Connection: keep-alive
{"eventId":"bdba69a9-1529-4edc-b362-e7a07d00d180","tiers":[{"id":"a93009fb-7af1-4daa-8540-059b0e1bbcb9","tierType":"GENERAL","price":60.0,"quota":20,"validFrom":null,"validUntil":null,"createdAt":"2026-04-08T03:53:21.059400931","updatedAt":"2026-04-08T03:53:21.059410826"}]}
188
Then status 201
0
189
* def tierId = response.tiers[0].id || response.tiers[0].tierId || response[0].id || response[0].tierId
0
191
Given url baseUrlEvents + '/api/v1/events/' + eventId + '/publish'
0
192
And header X-Role = 'ADMIN'
0
193
When method patch
11
22:53:21.066 request:
4 > PATCH http://localhost:8081/api/v1/events/bdba69a9-1529-4edc-b362-e7a07d00d180/publish
4 > X-Role: ADMIN
4 > Host: localhost:8081
4 > Connection: Keep-Alive
4 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
4 > Accept-Encoding: gzip,deflate
22:53:21.076 response time in milliseconds: 10
4 < 200
4 < Content-Type: application/json
4 < Transfer-Encoding: chunked
4 < Date: Wed, 08 Apr 2026 03:53:21 GMT
4 < Keep-Alive: timeout=60
4 < Connection: keep-alive
{"id":"bdba69a9-1529-4edc-b362-e7a07d00d180","roomId":"0021d028-c502-4196-a7bf-4619d2781416","title":"Advanced Lifecycle Test 2d26a2c9-fcb8-4021-b784-7ed761ced1f4 - Expired Payment Test","description":"Test payment on expired reservation","date":"2026-12-15T20:00:00","capacity":20,"status":"PUBLISHED","createdAt":"2026-04-08T03:53:21.047847","updatedAt":"2026-04-08T03:53:21.06992175","createdBy":"00000000-0000-0000-0000-000000000001","imageUrl":null,"subtitle":null,"location":null,"director":null,"castMembers":null,"duration":null,"tag":null,"isLimited":false,"isFeatured":false,"enableSeats":false,"author":null}
194
Then status 200
0
# Create reservation
197
* def buyerId2 = UUID.randomUUID() + ''
0
198
* def buyerEmail2 = 'buyer2-' + buyerId2.substring(0,8) + '@karate-test.com'
0
199
Given url baseUrlTicketing + '/api/v1/reservations'
0
200
And header X-User-Id = buyerId2
0
201
And request
0
{
"eventId": "#(eventId)",
"tierId": "#(tierId)",
"buyerEmail": "#(buyerEmail2)"
}
209
When method post
31
22:53:21.078 request:
5 > POST http://localhost:8082/api/v1/reservations
5 > X-User-Id: bffd7e23-677c-430c-8d59-d25e2b95d19c
5 > Content-Type: application/json; charset=UTF-8
5 > Content-Length: 145
5 > Host: localhost:8082
5 > Connection: Keep-Alive
5 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
5 > Accept-Encoding: gzip,deflate
{"eventId":"bdba69a9-1529-4edc-b362-e7a07d00d180","tierId":"a93009fb-7af1-4daa-8540-059b0e1bbcb9","buyerEmail":"buyer2-bffd7e23@karate-test.com"}
22:53:21.108 response time in milliseconds: 29
5 < 201
5 < Content-Type: application/json
5 < Transfer-Encoding: chunked
5 < Date: Wed, 08 Apr 2026 03:53:21 GMT
5 < Keep-Alive: timeout=60
5 < Connection: keep-alive
{"id":"0a568d8c-8960-4ff1-9fe1-09834d32bd07","eventId":"bdba69a9-1529-4edc-b362-e7a07d00d180","tierId":"a93009fb-7af1-4daa-8540-059b0e1bbcb9","buyerId":"bffd7e23-677c-430c-8d59-d25e2b95d19c","status":"PENDING","createdAt":"2026-04-08T03:53:21.101525766","updatedAt":"2026-04-08T03:53:21.101525766","validUntilAt":"2026-04-08T04:03:21.101525766"}
210
Then status 201
0
211
* def reservationId2 = response.id
0
212
* print 'Reservation created:', reservationId2
0
22:53:21.108 [print] Reservation created: 0a568d8c-8960-4ff1-9fe1-09834d32bd07
# Force expiration via API
215
Given url baseUrlTicketing + '/api/v1/testability/clock/advance'
0
216
And param minutes = 15
0
217
When method post
3
22:53:21.109 request:
6 > POST http://localhost:8082/api/v1/testability/clock/advance?minutes=15
6 > Host: localhost:8082
6 > Connection: Keep-Alive
6 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
6 > Accept-Encoding: gzip,deflate
22:53:21.111 response time in milliseconds: 2
6 < 200
6 < Content-Type: application/json
6 < Transfer-Encoding: chunked
6 < Date: Wed, 08 Apr 2026 03:53:21 GMT
6 < Keep-Alive: timeout=60
6 < Connection: keep-alive
{"status":"advanced","current_time":"2026-04-08T04:08:21.110963899"}
218
Then status 200
0
220
Given url baseUrlTicketing + '/api/v1/testability/jobs/expiration/trigger'
0
221
When method post
33
22:53:21.113 request:
7 > POST http://localhost:8082/api/v1/testability/jobs/expiration/trigger
7 > Host: localhost:8082
7 > Connection: Keep-Alive
7 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
7 > Accept-Encoding: gzip,deflate
22:53:21.145 response time in milliseconds: 32
7 < 200
7 < Content-Type: application/json
7 < Transfer-Encoding: chunked
7 < Date: Wed, 08 Apr 2026 03:53:21 GMT
7 < Keep-Alive: timeout=60
7 < Connection: keep-alive
{"status":"Triggered processExpiredBatch"}
222
Then status 200
0
224
Given url baseUrlTicketing + '/api/v1/reservations/' + reservationId2
0
225
And header X-User-Id = buyerId2
0
226
When method get
6
22:53:21.146 request:
8 > GET http://localhost:8082/api/v1/reservations/0a568d8c-8960-4ff1-9fe1-09834d32bd07
8 > X-User-Id: bffd7e23-677c-430c-8d59-d25e2b95d19c
8 > Host: localhost:8082
8 > Connection: Keep-Alive
8 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
8 > Accept-Encoding: gzip,deflate
22:53:21.151 response time in milliseconds: 5
8 < 200
8 < Content-Type: application/json
8 < Transfer-Encoding: chunked
8 < Date: Wed, 08 Apr 2026 03:53:21 GMT
8 < Keep-Alive: timeout=60
8 < Connection: keep-alive
{"id":"0a568d8c-8960-4ff1-9fe1-09834d32bd07","eventId":"bdba69a9-1529-4edc-b362-e7a07d00d180","tierId":"a93009fb-7af1-4daa-8540-059b0e1bbcb9","status":"EXPIRED","createdAt":"2026-04-08T03:53:21.101526","updatedAt":"2026-04-08T03:53:21.135208","validUntilAt":"2026-04-08T04:03:21.101526"}
227
Then status 200
0
228
* match response.status == 'EXPIRED'
0
# HU-05 Negative: Attempt payment on expired reservation (should fail)
231
Given url baseUrlTicketing + '/api/v1/reservations/' + reservationId2 + '/payments'
0
232
And header X-User-Id = buyerId2
0
233
And request
0
{
"amount": 60.00,
"paymentMethod": "MOCK",
"status": "APPROVED"
}
241
When method post
33
22:53:21.155 request:
9 > POST http://localhost:8082/api/v1/reservations/0a568d8c-8960-4ff1-9fe1-09834d32bd07/payments
9 > X-User-Id: bffd7e23-677c-430c-8d59-d25e2b95d19c
9 > Content-Type: application/json; charset=UTF-8
9 > Content-Length: 58
9 > Host: localhost:8082
9 > Connection: Keep-Alive
9 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
9 > Accept-Encoding: gzip,deflate
{"amount":60.0,"paymentMethod":"MOCK","status":"APPROVED"}
22:53:21.185 response time in milliseconds: 30
9 < 400
9 < Content-Type: application/json
9 < Transfer-Encoding: chunked
9 < Date: Wed, 08 Apr 2026 03:53:21 GMT
9 < Connection: close
{"error":"Reservation has expired","timestamp":"2026-04-08T03:53:21.184448273"}
242
Then status 400
0
243
* print 'Payment rejected on expired reservation (✅ expected behavior)'
0
22:53:21.186 [print] Payment rejected on expired reservation (✅ expected behavior)
# Cleanup Clock
246
Given url baseUrlTicketing + '/api/v1/testability/clock/reset'
0
247
When method post
3
22:53:21.187 request:
10 > POST http://localhost:8082/api/v1/testability/clock/reset
10 > Host: localhost:8082
10 > Connection: Keep-Alive
10 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
10 > Accept-Encoding: gzip,deflate
22:53:21.189 response time in milliseconds: 2
10 < 200
10 < Content-Type: application/json
10 < Transfer-Encoding: chunked
10 < Date: Wed, 08 Apr 2026 03:53:21 GMT
10 < Keep-Alive: timeout=60
10 < Connection: keep-alive
{"status":"reset","current_time":"2026-04-08T03:53:21.188669281"}
248
Then status 200
0
249
* print '✅ Scenario 2 PASS: Payment on expired reservation rejected'
0
22:53:21.190 [print] ✅ Scenario 2 PASS: Payment on expired reservation rejected
Scenario: [3:252]
Scenario 3 - Concurrency on Last Available Slot
ms: 175
>>
Background:
7
* def baseUrlEvents = karate.properties['baseUrlEvents'] || 'http://localhost:8081'
0
22:53:21.193 karate.env system property was: null
8
* def baseUrlTicketing = karate.properties['baseUrlTicketing'] || 'http://localhost:8082'
0
9
* def UUID = Java.type('java.util.UUID')
0
10
* def eventTitle = 'Advanced Lifecycle Test ' + UUID.randomUUID() + ''
0
11
* def futureDate = '2026-12-15T20:00:00'
0
# Setup: Create room
255
Given url baseUrlEvents + '/api/v1/rooms'
0
256
And header X-Role = 'ADMIN'
0
257
And header X-User-Id = '00000000-0000-0000-0000-000000000001'
0
258
And request
0
{
"name": "Concurrency Test Room",
"maxCapacity": 100
}
265
When method post
9
22:53:21.197 request:
1 > POST http://localhost:8081/api/v1/rooms
1 > X-Role: ADMIN
1 > X-User-Id: 00000000-0000-0000-0000-000000000001
1 > Content-Type: application/json; charset=UTF-8
1 > Content-Length: 50
1 > Host: localhost:8081
1 > Connection: Keep-Alive
1 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
1 > Accept-Encoding: gzip,deflate
{"name":"Concurrency Test Room","maxCapacity":100}
22:53:21.205 response time in milliseconds: 8
1 < 201
1 < Content-Type: application/json
1 < Transfer-Encoding: chunked
1 < Date: Wed, 08 Apr 2026 03:53:21 GMT
1 < Keep-Alive: timeout=60
1 < Connection: keep-alive
{"id":"1e8896c9-5c87-4114-a9a4-7bcb5a644368","name":"Concurrency Test Room","maxCapacity":100,"created_at":"2026-04-08T03:53:21.199725411","updated_at":"2026-04-08T03:53:21.199739992"}
266
Then status 201
0
267
* def roomId = response.id ? response.id : response.roomId
0
# Setup: Create event with capacity=2 (only 2 slots, to trigger concurrency)
270
* def scenarioTitle = eventTitle + ' - Concurrency Test'
0
271
Given url baseUrlEvents + '/api/v1/events'
0
272
And header X-Role = 'ADMIN'
0
273
And header X-User-Id = '00000000-0000-0000-0000-000000000001'
0
274
And request
0
{
"roomId": "#(roomId)",
"title": "#(scenarioTitle)",
"description": "Test concurrency on last slot",
"date": "#(futureDate)",
"capacity": 2,
"enableSeats": false
}
285
When method post
11
22:53:21.207 request:
2 > POST http://localhost:8081/api/v1/events
2 > X-Role: ADMIN
2 > X-User-Id: 00000000-0000-0000-0000-000000000001
2 > Content-Type: application/json; charset=UTF-8
2 > Content-Length: 247
2 > Host: localhost:8081
2 > Connection: Keep-Alive
2 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
2 > Accept-Encoding: gzip,deflate
{"roomId":"1e8896c9-5c87-4114-a9a4-7bcb5a644368","title":"Advanced Lifecycle Test a87d3c4b-1ab1-4111-bd52-e40ca71c64ef - Concurrency Test","description":"Test concurrency on last slot","date":"2026-12-15T20:00:00","capacity":2,"enableSeats":false}
22:53:21.217 response time in milliseconds: 10
2 < 201
2 < Content-Type: application/json
2 < Transfer-Encoding: chunked
2 < Date: Wed, 08 Apr 2026 03:53:21 GMT
2 < Keep-Alive: timeout=60
2 < Connection: keep-alive
{"id":"b582320c-ef2a-49a3-970e-999fac4f08f5","roomId":"1e8896c9-5c87-4114-a9a4-7bcb5a644368","title":"Advanced Lifecycle Test a87d3c4b-1ab1-4111-bd52-e40ca71c64ef - Concurrency Test","description":"Test concurrency on last slot","date":"2026-12-15T20:00:00","capacity":2,"status":"DRAFT","createdAt":"2026-04-08T03:53:21.211225559","updatedAt":"2026-04-08T03:53:21.211240488","createdBy":"00000000-0000-0000-0000-000000000001","imageUrl":null,"subtitle":null,"location":null,"director":null,"castMembers":null,"duration":null,"tag":null,"isLimited":false,"isFeatured":false,"enableSeats":false,"author":null}
286
Then status 201
0
287
* def eventId = response.id ? response.id : response.eventId
0
# Setup: Create tier with quota=2
290
Given url baseUrlEvents + '/api/v1/events/' + eventId + '/tiers'
0
291
And header X-Role = 'ADMIN'
0
292
And request
0
[
{
"tierType": "GENERAL",
"price": 70.00,
"quota": 2
}
]
302
When method post
15
22:53:21.218 request:
3 > POST http://localhost:8081/api/v1/events/b582320c-ef2a-49a3-970e-999fac4f08f5/tiers
3 > X-Role: ADMIN
3 > Content-Type: application/json; charset=UTF-8
3 > Content-Length: 47
3 > Host: localhost:8081
3 > Connection: Keep-Alive
3 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
3 > Accept-Encoding: gzip,deflate
[{"tierType":"GENERAL","price":70.0,"quota":2}]
22:53:21.232 response time in milliseconds: 14
3 < 201
3 < Content-Type: application/json
3 < Transfer-Encoding: chunked
3 < Date: Wed, 08 Apr 2026 03:53:21 GMT
3 < Keep-Alive: timeout=60
3 < Connection: keep-alive
{"eventId":"b582320c-ef2a-49a3-970e-999fac4f08f5","tiers":[{"id":"f6bc2758-e5ae-4a44-b75f-32f2866f1905","tierType":"GENERAL","price":70.0,"quota":2,"validFrom":null,"validUntil":null,"createdAt":"2026-04-08T03:53:21.224254643","updatedAt":"2026-04-08T03:53:21.224264278"}]}
303
Then status 201
0
304
* def tierId = response.tiers[0].id || response.tiers[0].tierId || response[0].id || response[0].tierId
0
305
* print 'Tier created with quota=2 (limited slots)'
0
22:53:21.233 [print] Tier created with quota=2 (limited slots)
# Publish
308
Given url baseUrlEvents + '/api/v1/events/' + eventId + '/publish'
0
309
And header X-Role = 'ADMIN'
0
310
When method patch
12
22:53:21.234 request:
4 > PATCH http://localhost:8081/api/v1/events/b582320c-ef2a-49a3-970e-999fac4f08f5/publish
4 > X-Role: ADMIN
4 > Host: localhost:8081
4 > Connection: Keep-Alive
4 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
4 > Accept-Encoding: gzip,deflate
22:53:21.244 response time in milliseconds: 10
4 < 200
4 < Content-Type: application/json
4 < Transfer-Encoding: chunked
4 < Date: Wed, 08 Apr 2026 03:53:21 GMT
4 < Keep-Alive: timeout=60
4 < Connection: keep-alive
{"id":"b582320c-ef2a-49a3-970e-999fac4f08f5","roomId":"1e8896c9-5c87-4114-a9a4-7bcb5a644368","title":"Advanced Lifecycle Test a87d3c4b-1ab1-4111-bd52-e40ca71c64ef - Concurrency Test","description":"Test concurrency on last slot","date":"2026-12-15T20:00:00","capacity":2,"status":"PUBLISHED","createdAt":"2026-04-08T03:53:21.211226","updatedAt":"2026-04-08T03:53:21.237917804","createdBy":"00000000-0000-0000-0000-000000000001","imageUrl":null,"subtitle":null,"location":null,"director":null,"castMembers":null,"duration":null,"tag":null,"isLimited":false,"isFeatured":false,"enableSeats":false,"author":null}
311
Then status 200
0
# Buyer 1: Reserve + Pay APPROVED (consumes 1 slot)
314
* def buyer1 = UUID.randomUUID() + ''
1
315
* def buyer1Email = 'buyer1-conc-' + buyer1.substring(0,8) + '@karate-test.com'
0
316
Given url baseUrlTicketing + '/api/v1/reservations'
0
317
And header X-User-Id = buyer1
0
318
And request
0
{
"eventId": "#(eventId)",
"tierId": "#(tierId)",
"buyerEmail": "#(buyer1Email)"
}
326
When method post
30
22:53:21.246 request:
5 > POST http://localhost:8082/api/v1/reservations
5 > X-User-Id: f862c26a-3203-4231-bd72-c5d240efdfb6
5 > Content-Type: application/json; charset=UTF-8
5 > Content-Length: 150
5 > Host: localhost:8082
5 > Connection: Keep-Alive
5 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
5 > Accept-Encoding: gzip,deflate
{"eventId":"b582320c-ef2a-49a3-970e-999fac4f08f5","tierId":"f6bc2758-e5ae-4a44-b75f-32f2866f1905","buyerEmail":"buyer1-conc-f862c26a@karate-test.com"}
22:53:21.275 response time in milliseconds: 29
5 < 201
5 < Content-Type: application/json
5 < Transfer-Encoding: chunked
5 < Date: Wed, 08 Apr 2026 03:53:21 GMT
5 < Keep-Alive: timeout=60
5 < Connection: keep-alive
{"id":"a2547df1-8eec-4c96-9cb6-41e51a85e784","eventId":"b582320c-ef2a-49a3-970e-999fac4f08f5","tierId":"f6bc2758-e5ae-4a44-b75f-32f2866f1905","buyerId":"f862c26a-3203-4231-bd72-c5d240efdfb6","status":"PENDING","createdAt":"2026-04-08T03:53:21.269270205","updatedAt":"2026-04-08T03:53:21.269270205","validUntilAt":"2026-04-08T04:03:21.269270205"}
327
Then status 201
0
328
* def res1 = response.id
0
330
Given url baseUrlTicketing + '/api/v1/reservations/' + res1 + '/payments'
0
331
And header X-User-Id = buyer1
0
332
And request
0
{
"amount": 70.00,
"paymentMethod": "MOCK",
"status": "APPROVED"
}
340
When method post
29
22:53:21.277 request:
6 > POST http://localhost:8082/api/v1/reservations/a2547df1-8eec-4c96-9cb6-41e51a85e784/payments
6 > X-User-Id: f862c26a-3203-4231-bd72-c5d240efdfb6
6 > Content-Type: application/json; charset=UTF-8
6 > Content-Length: 58
6 > Host: localhost:8082
6 > Connection: Keep-Alive
6 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
6 > Accept-Encoding: gzip,deflate
{"amount":70.0,"paymentMethod":"MOCK","status":"APPROVED"}
22:53:21.305 response time in milliseconds: 28
6 < 200
6 < Content-Type: application/json
6 < Transfer-Encoding: chunked
6 < Date: Wed, 08 Apr 2026 03:53:21 GMT
6 < Keep-Alive: timeout=60
6 < Connection: keep-alive
{"reservationId":"a2547df1-8eec-4c96-9cb6-41e51a85e784","status":"CONFIRMED","ticketId":"81caf140-5861-45f1-8336-09a6d61d0664","message":"Payment approved. Ticket generated.","ticket":{"ticketId":"81caf140-5861-45f1-8336-09a6d61d0664","eventId":"b582320c-ef2a-49a3-970e-999fac4f08f5","eventTitle":"Advanced Lifecycle Test a87d3c4b-1ab1-4111-bd52-e40ca71c64ef - Concurrency Test","eventDate":"2026-12-15T20:00:00","tier":"GENERAL","pricePaid":70.0,"status":"VALID","purchasedAt":"2026-04-08T03:53:21.288433011","buyerEmail":"buyer1-conc-f862c26a@karate-test.com","reservationId":"a2547df1-8eec-4c96-9cb6-41e51a85e784"},"timestamp":"2026-04-08T03:53:21.296571361"}
341
Then status 200
0
342
* print 'Buyer 1 payment approved (1/2 slots consumed)'
0
22:53:21.306 [print] Buyer 1 payment approved (1/2 slots consumed)
# Buyer 2: Reserve + Pay APPROVED (consumes last slot)
345
* def buyer2 = UUID.randomUUID() + ''
0
346
* def buyer2Email = 'buyer2-conc-' + buyer2.substring(0,8) + '@karate-test.com'
0
347
Given url baseUrlTicketing + '/api/v1/reservations'
0
348
And header X-User-Id = buyer2
0
349
And request
0
{
"eventId": "#(eventId)",
"tierId": "#(tierId)",
"buyerEmail": "#(buyer2Email)"
}
357
When method post
30
22:53:21.307 request:
7 > POST http://localhost:8082/api/v1/reservations
7 > X-User-Id: f68fbcc9-5a5b-47a9-8743-71f40426c3f6
7 > Content-Type: application/json; charset=UTF-8
7 > Content-Length: 150
7 > Host: localhost:8082
7 > Connection: Keep-Alive
7 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
7 > Accept-Encoding: gzip,deflate
{"eventId":"b582320c-ef2a-49a3-970e-999fac4f08f5","tierId":"f6bc2758-e5ae-4a44-b75f-32f2866f1905","buyerEmail":"buyer2-conc-f68fbcc9@karate-test.com"}
22:53:21.336 response time in milliseconds: 29
7 < 201
7 < Content-Type: application/json
7 < Transfer-Encoding: chunked
7 < Date: Wed, 08 Apr 2026 03:53:21 GMT
7 < Keep-Alive: timeout=60
7 < Connection: keep-alive
{"id":"f3c69043-133f-4858-ba32-088df6494060","eventId":"b582320c-ef2a-49a3-970e-999fac4f08f5","tierId":"f6bc2758-e5ae-4a44-b75f-32f2866f1905","buyerId":"f68fbcc9-5a5b-47a9-8743-71f40426c3f6","status":"PENDING","createdAt":"2026-04-08T03:53:21.329867156","updatedAt":"2026-04-08T03:53:21.329867156","validUntilAt":"2026-04-08T04:03:21.329867156"}
358
Then status 201
0
359
* def res2 = response.id
0
361
Given url baseUrlTicketing + '/api/v1/reservations/' + res2 + '/payments'
0
362
And header X-User-Id = buyer2
0
363
And request
0
{
"amount": 70.00,
"paymentMethod": "MOCK",
"status": "APPROVED"
}
371
When method post
27
22:53:21.337 request:
8 > POST http://localhost:8082/api/v1/reservations/f3c69043-133f-4858-ba32-088df6494060/payments
8 > X-User-Id: f68fbcc9-5a5b-47a9-8743-71f40426c3f6
8 > Content-Type: application/json; charset=UTF-8
8 > Content-Length: 58
8 > Host: localhost:8082
8 > Connection: Keep-Alive
8 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
8 > Accept-Encoding: gzip,deflate
{"amount":70.0,"paymentMethod":"MOCK","status":"APPROVED"}
22:53:21.364 response time in milliseconds: 27
8 < 200
8 < Content-Type: application/json
8 < Transfer-Encoding: chunked
8 < Date: Wed, 08 Apr 2026 03:53:21 GMT
8 < Keep-Alive: timeout=60
8 < Connection: keep-alive
{"reservationId":"f3c69043-133f-4858-ba32-088df6494060","status":"CONFIRMED","ticketId":"67b3ecf9-6c91-426a-8734-c944251a89b0","message":"Payment approved. Ticket generated.","ticket":{"ticketId":"67b3ecf9-6c91-426a-8734-c944251a89b0","eventId":"b582320c-ef2a-49a3-970e-999fac4f08f5","eventTitle":"Advanced Lifecycle Test a87d3c4b-1ab1-4111-bd52-e40ca71c64ef - Concurrency Test","eventDate":"2026-12-15T20:00:00","tier":"GENERAL","pricePaid":70.0,"status":"VALID","purchasedAt":"2026-04-08T03:53:21.348399436","buyerEmail":"buyer2-conc-f68fbcc9@karate-test.com","reservationId":"f3c69043-133f-4858-ba32-088df6494060"},"timestamp":"2026-04-08T03:53:21.355629064"}
372
Then status 200
0
373
* print 'Buyer 2 payment approved (2/2 slots consumed - fully booked)'
0
22:53:21.364 [print] Buyer 2 payment approved (2/2 slots consumed - fully booked)
# Verify tier is exhausted
376
Given url baseUrlEvents + '/api/v1/events/' + eventId + '/tiers'
0
377
When method get
5
22:53:21.365 request:
9 > GET http://localhost:8081/api/v1/events/b582320c-ef2a-49a3-970e-999fac4f08f5/tiers
9 > Host: localhost:8081
9 > Connection: Keep-Alive
9 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
9 > Accept-Encoding: gzip,deflate
22:53:21.370 response time in milliseconds: 4
9 < 200
9 < Content-Type: application/json
9 < Transfer-Encoding: chunked
9 < Date: Wed, 08 Apr 2026 03:53:21 GMT
9 < Keep-Alive: timeout=60
9 < Connection: keep-alive
{"eventId":"b582320c-ef2a-49a3-970e-999fac4f08f5","tiers":[{"id":"f6bc2758-e5ae-4a44-b75f-32f2866f1905","tierType":"GENERAL","price":70.00,"quota":0,"validFrom":null,"validUntil":null,"createdAt":"2026-04-08T03:53:21.224255","updatedAt":"2026-04-08T03:53:21.321322"}]}
378
Then status 200
0
379
* def tierFinal = response.tiers[0]
0
380
* print 'Final tier state: quota=' + tierFinal.quota
0
22:53:21.370 [print] Final tier state: quota=0
381
* assert tierFinal.quota == 0
0
382
* print '✅ Scenario 3 PASS: Concurrency handled correctly, no overbooking'
0
22:53:21.371 [print] ✅ Scenario 3 PASS: Concurrency handled correctly, no overbooking
Scenario: [4:384]
Scenario 3b - Real Concurrency on Last Available Slot (TC-015)
ms: 180
>>
Background:
7
* def baseUrlEvents = karate.properties['baseUrlEvents'] || 'http://localhost:8081'
0
22:53:21.372 karate.env system property was: null
8
* def baseUrlTicketing = karate.properties['baseUrlTicketing'] || 'http://localhost:8082'
0
9
* def UUID = Java.type('java.util.UUID')
0
10
* def eventTitle = 'Advanced Lifecycle Test ' + UUID.randomUUID() + ''
0
11
* def futureDate = '2026-12-15T20:00:00'
0
# Setup: Create room
387
Given url baseUrlEvents + '/api/v1/rooms'
0
388
And header X-Role = 'ADMIN'
0
389
And header X-User-Id = '00000000-0000-0000-0000-000000000001'
0
390
And request
0
{
"name": "Real Concurrency Test Room",
"maxCapacity": 100
}
397
When method post
9
22:53:21.373 request:
1 > POST http://localhost:8081/api/v1/rooms
1 > X-Role: ADMIN
1 > X-User-Id: 00000000-0000-0000-0000-000000000001
1 > Content-Type: application/json; charset=UTF-8
1 > Content-Length: 55
1 > Host: localhost:8081
1 > Connection: Keep-Alive
1 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
1 > Accept-Encoding: gzip,deflate
{"name":"Real Concurrency Test Room","maxCapacity":100}
22:53:21.382 response time in milliseconds: 8
1 < 201
1 < Content-Type: application/json
1 < Transfer-Encoding: chunked
1 < Date: Wed, 08 Apr 2026 03:53:21 GMT
1 < Keep-Alive: timeout=60
1 < Connection: keep-alive
{"id":"1445720e-3423-47b4-913a-63a935532ad5","name":"Real Concurrency Test Room","maxCapacity":100,"created_at":"2026-04-08T03:53:21.375601669","updated_at":"2026-04-08T03:53:21.37561202"}
398
Then status 201
0
399
* def roomId = response.id ? response.id : response.roomId
0
401
* def scenario3bTitle = eventTitle + ' - Real Conc Test'
0
402
Given url baseUrlEvents + '/api/v1/events'
0
403
And header X-Role = 'ADMIN'
0
404
And header X-User-Id = '00000000-0000-0000-0000-000000000001'
0
405
And request
0
{
"roomId": "#(roomId)",
"title": "#(scenario3bTitle)",
"description": "Test real concurrency on last slot",
"date": "#(futureDate)",
"capacity": 1,
"enableSeats": false
}
416
When method post
12
22:53:21.383 request:
2 > POST http://localhost:8081/api/v1/events
2 > X-Role: ADMIN
2 > X-User-Id: 00000000-0000-0000-0000-000000000001
2 > Content-Type: application/json; charset=UTF-8
2 > Content-Length: 250
2 > Host: localhost:8081
2 > Connection: Keep-Alive
2 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
2 > Accept-Encoding: gzip,deflate
{"roomId":"1445720e-3423-47b4-913a-63a935532ad5","title":"Advanced Lifecycle Test efcaf3a1-e654-4290-bc6b-34063eea5872 - Real Conc Test","description":"Test real concurrency on last slot","date":"2026-12-15T20:00:00","capacity":1,"enableSeats":false}
22:53:21.395 response time in milliseconds: 10
2 < 201
2 < Content-Type: application/json
2 < Transfer-Encoding: chunked
2 < Date: Wed, 08 Apr 2026 03:53:21 GMT
2 < Keep-Alive: timeout=60
2 < Connection: keep-alive
{"id":"0b242e7a-1efa-4d94-b172-e0017f1a3982","roomId":"1445720e-3423-47b4-913a-63a935532ad5","title":"Advanced Lifecycle Test efcaf3a1-e654-4290-bc6b-34063eea5872 - Real Conc Test","description":"Test real concurrency on last slot","date":"2026-12-15T20:00:00","capacity":1,"status":"DRAFT","createdAt":"2026-04-08T03:53:21.3880331","updatedAt":"2026-04-08T03:53:21.388039434","createdBy":"00000000-0000-0000-0000-000000000001","imageUrl":null,"subtitle":null,"location":null,"director":null,"castMembers":null,"duration":null,"tag":null,"isLimited":false,"isFeatured":false,"enableSeats":false,"author":null}
417
Then status 201
0
418
* def eventId = response.id ? response.id : response.eventId
0
# Setup: Create tier with exact quota=1
421
Given url baseUrlEvents + '/api/v1/events/' + eventId + '/tiers'
0
422
And header X-Role = 'ADMIN'
0
423
And header X-User-Id = '00000000-0000-0000-0000-000000000001'
0
424
And request
0
[
{
"tierType": "GENERAL",
"price": 70.00,
"quota": 1
}
]
434
When method post
12
22:53:21.396 request:
3 > POST http://localhost:8081/api/v1/events/0b242e7a-1efa-4d94-b172-e0017f1a3982/tiers
3 > X-Role: ADMIN
3 > X-User-Id: 00000000-0000-0000-0000-000000000001
3 > Content-Type: application/json; charset=UTF-8
3 > Content-Length: 47
3 > Host: localhost:8081
3 > Connection: Keep-Alive
3 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
3 > Accept-Encoding: gzip,deflate
[{"tierType":"GENERAL","price":70.0,"quota":1}]
22:53:21.407 response time in milliseconds: 11
3 < 201
3 < Content-Type: application/json
3 < Transfer-Encoding: chunked
3 < Date: Wed, 08 Apr 2026 03:53:21 GMT
3 < Keep-Alive: timeout=60
3 < Connection: keep-alive
{"eventId":"0b242e7a-1efa-4d94-b172-e0017f1a3982","tiers":[{"id":"a4be62ea-bfc0-4ecd-a4b5-2a7d4d2af2ca","tierType":"GENERAL","price":70.0,"quota":1,"validFrom":null,"validUntil":null,"createdAt":"2026-04-08T03:53:21.400609434","updatedAt":"2026-04-08T03:53:21.400617995"}]}
435
Then status 201
0
436
* def tierBlock = response.tiers ? response.tiers[0] : response[0]
0
437
* def tierId = tierBlock.id ? tierBlock.id : tierBlock.tierId
0
# Publish
440
Given url baseUrlEvents + '/api/v1/events/' + eventId + '/publish'
0
441
And header X-Role = 'ADMIN'
0
442
And header X-User-Id = '00000000-0000-0000-0000-000000000001'
0
443
When method patch
11
22:53:21.409 request:
4 > PATCH http://localhost:8081/api/v1/events/0b242e7a-1efa-4d94-b172-e0017f1a3982/publish
4 > X-Role: ADMIN
4 > X-User-Id: 00000000-0000-0000-0000-000000000001
4 > Host: localhost:8081
4 > Connection: Keep-Alive
4 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
4 > Accept-Encoding: gzip,deflate
22:53:21.419 response time in milliseconds: 10
4 < 200
4 < Content-Type: application/json
4 < Transfer-Encoding: chunked
4 < Date: Wed, 08 Apr 2026 03:53:21 GMT
4 < Keep-Alive: timeout=60
4 < Connection: keep-alive
{"id":"0b242e7a-1efa-4d94-b172-e0017f1a3982","roomId":"1445720e-3423-47b4-913a-63a935532ad5","title":"Advanced Lifecycle Test efcaf3a1-e654-4290-bc6b-34063eea5872 - Real Conc Test","description":"Test real concurrency on last slot","date":"2026-12-15T20:00:00","capacity":1,"status":"PUBLISHED","createdAt":"2026-04-08T03:53:21.388033","updatedAt":"2026-04-08T03:53:21.41250092","createdBy":"00000000-0000-0000-0000-000000000001","imageUrl":null,"subtitle":null,"location":null,"director":null,"castMembers":null,"duration":null,"tag":null,"isLimited":false,"isFeatured":false,"enableSeats":false,"author":null}
444
Then status 200
0
# Act: Launch two concurrent requests using java.util.concurrent and HttpClient
447
* def HttpClient = Java.type('java.net.http.HttpClient')
9
448
* def HttpRequest = Java.type('java.net.http.HttpRequest')
0
449
* def HttpResponse = Java.type('java.net.http.HttpResponse')
0
450
* def URI = Java.type('java.net.URI')
0
451
* def BodyPublishers = Java.type('java.net.http.HttpRequest.BodyPublishers')
1
452
* def BodyHandlers = Java.type('java.net.http.HttpResponse.BodyHandlers')
1
453
* def CompletableFuture = Java.type('java.util.concurrent.CompletableFuture')
0
455
* def buyer1C = UUID.randomUUID() + ''
0
456
* def buyer2C = UUID.randomUUID() + ''
0
458
* def body1 = '{"eventId":"' + eventId + '","tierId":"' + tierId + '","buyerEmail":"c1-' + buyer1C.substring(0,8) + '@test.com"}'
0
459
* def body2 = '{"eventId":"' + eventId + '","tierId":"' + tierId + '","buyerEmail":"c2-' + buyer2C.substring(0,8) + '@test.com"}'
0
461
* def client = HttpClient.newHttpClient()
34
462
* def req1 = HttpRequest.newBuilder().uri(new URI(baseUrlTicketing + '/api/v1/reservations')).header('Content-Type', 'application/json').header('X-User-Id', buyer1C).POST(BodyPublishers.ofString(body1)).build()
6
463
* def req2 = HttpRequest.newBuilder().uri(new URI(baseUrlTicketing + '/api/v1/reservations')).header('Content-Type', 'application/json').header('X-User-Id', buyer2C).POST(BodyPublishers.ofString(body2)).build()
1
465
* def future1 = client.sendAsync(req1, BodyHandlers.ofString())
6
466
* def future2 = client.sendAsync(req2, BodyHandlers.ofString())
1
467
* CompletableFuture.allOf(future1, future2).join()
61
469
* def resp1 = future1.get()
1
470
* def resp2 = future2.get()
0
471
* def code1 = parseInt(resp1.statusCode() + '')
4
472
* def code2 = parseInt(resp2.statusCode() + '')
0
473
* print 'Request 1 returned:', code1
0
22:53:21.546 [print] Request 1 returned: 409
474
* print 'Request 2 returned:', code2
0
22:53:21.546 [print] Request 2 returned: 201
476
* assert (code1 == 201 && (code2 == 409 || code2 == 400)) || (code2 == 201 && (code1 == 409 || code1 == 400))
0
# Determine winner
479
* def winner = code1 == 201 ? 'Buyer 1' : 'Buyer 2'
0
480
* def loser = code1 != 201 ? 'Buyer 1' : 'Buyer 2'
0
481
* print 'Concurrency winner:', winner, '| loser:', loser
0
22:53:21.546 [print] Concurrency winner: Buyer 2 | loser: Buyer 1
# Verify tier is exhausted
484
Given url baseUrlEvents + '/api/v1/events/' + eventId + '/tiers'
0
485
When method get
7
22:53:21.547 request:
5 > GET http://localhost:8081/api/v1/events/0b242e7a-1efa-4d94-b172-e0017f1a3982/tiers
5 > Host: localhost:8081
5 > Connection: Keep-Alive
5 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
5 > Accept-Encoding: gzip,deflate
22:53:21.553 response time in milliseconds: 5
5 < 200
5 < Content-Type: application/json
5 < Transfer-Encoding: chunked
5 < Date: Wed, 08 Apr 2026 03:53:21 GMT
5 < Keep-Alive: timeout=60
5 < Connection: keep-alive
{"eventId":"0b242e7a-1efa-4d94-b172-e0017f1a3982","tiers":[{"id":"a4be62ea-bfc0-4ecd-a4b5-2a7d4d2af2ca","tierType":"GENERAL","price":70.00,"quota":0,"validFrom":null,"validUntil":null,"createdAt":"2026-04-08T03:53:21.400609","updatedAt":"2026-04-08T03:53:21.504245"}]}
486
Then status 200
0
487
* assert response.tiers[0].quota == 0
0
488
* print '✅ TC-015 Scenario 3b PASS: Real concurrency handled correctly'
0
22:53:21.553 [print] ✅ TC-015 Scenario 3b PASS: Real concurrency handled correctly
Scenario: [5:491]
Scenario 4 - Confirmed Purchase Must Not Be Released by Scheduler
ms: 90259
>>
Background:
7
* def baseUrlEvents = karate.properties['baseUrlEvents'] || 'http://localhost:8081'
0
22:53:21.555 karate.env system property was: null
8
* def baseUrlTicketing = karate.properties['baseUrlTicketing'] || 'http://localhost:8082'
0
9
* def UUID = Java.type('java.util.UUID')
0
10
* def eventTitle = 'Advanced Lifecycle Test ' + UUID.randomUUID() + ''
0
11
* def futureDate = '2026-12-15T20:00:00'
0
# Setup: Create room
494
Given url baseUrlEvents + '/api/v1/rooms'
0
495
And header X-Role = 'ADMIN'
0
496
And header X-User-Id = '00000000-0000-0000-0000-000000000001'
0
497
And request
0
{
"name": "Confirmed Purchase Protection Room",
"maxCapacity": 100
}
504
When method post
8
22:53:21.556 request:
1 > POST http://localhost:8081/api/v1/rooms
1 > X-Role: ADMIN
1 > X-User-Id: 00000000-0000-0000-0000-000000000001
1 > Content-Type: application/json; charset=UTF-8
1 > Content-Length: 63
1 > Host: localhost:8081
1 > Connection: Keep-Alive
1 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
1 > Accept-Encoding: gzip,deflate
{"name":"Confirmed Purchase Protection Room","maxCapacity":100}
22:53:21.564 response time in milliseconds: 7
1 < 201
1 < Content-Type: application/json
1 < Transfer-Encoding: chunked
1 < Date: Wed, 08 Apr 2026 03:53:21 GMT
1 < Keep-Alive: timeout=60
1 < Connection: keep-alive
{"id":"5a873bd7-822e-4199-87fa-4d440825d8e8","name":"Confirmed Purchase Protection Room","maxCapacity":100,"created_at":"2026-04-08T03:53:21.558145006","updated_at":"2026-04-08T03:53:21.558153481"}
505
Then status 201
0
506
* def roomId = response.id ? response.id : response.roomId
0
# Setup: Create event + tier + publish
509
* def scenarioTitle = eventTitle + ' - Confirmed Protection'
0
510
Given url baseUrlEvents + '/api/v1/events'
0
511
And header X-Role = 'ADMIN'
0
512
And header X-User-Id = '00000000-0000-0000-0000-000000000001'
0
513
And request
0
{
"roomId": "#(roomId)",
"title": "#(scenarioTitle)",
"description": "Test confirmed purchase is not released",
"date": "#(futureDate)",
"capacity": 50,
"enableSeats": false
}
524
When method post
11
22:53:21.565 request:
2 > POST http://localhost:8081/api/v1/events
2 > X-Role: ADMIN
2 > X-User-Id: 00000000-0000-0000-0000-000000000001
2 > Content-Type: application/json; charset=UTF-8
2 > Content-Length: 262
2 > Host: localhost:8081
2 > Connection: Keep-Alive
2 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
2 > Accept-Encoding: gzip,deflate
{"roomId":"5a873bd7-822e-4199-87fa-4d440825d8e8","title":"Advanced Lifecycle Test 192b4592-25e2-414d-b712-7d42e5643483 - Confirmed Protection","description":"Test confirmed purchase is not released","date":"2026-12-15T20:00:00","capacity":50,"enableSeats":false}
22:53:21.575 response time in milliseconds: 10
2 < 201
2 < Content-Type: application/json
2 < Transfer-Encoding: chunked
2 < Date: Wed, 08 Apr 2026 03:53:21 GMT
2 < Keep-Alive: timeout=60
2 < Connection: keep-alive
{"id":"07cf61a8-5dfb-476d-98da-0b6c7adadeb0","roomId":"5a873bd7-822e-4199-87fa-4d440825d8e8","title":"Advanced Lifecycle Test 192b4592-25e2-414d-b712-7d42e5643483 - Confirmed Protection","description":"Test confirmed purchase is not released","date":"2026-12-15T20:00:00","capacity":50,"status":"DRAFT","createdAt":"2026-04-08T03:53:21.569348678","updatedAt":"2026-04-08T03:53:21.569361266","createdBy":"00000000-0000-0000-0000-000000000001","imageUrl":null,"subtitle":null,"location":null,"director":null,"castMembers":null,"duration":null,"tag":null,"isLimited":false,"isFeatured":false,"enableSeats":false,"author":null}
525
Then status 201
0
526
* def eventId = response.id ? response.id : response.eventId
0
528
Given url baseUrlEvents + '/api/v1/events/' + eventId + '/tiers'
0
529
And header X-Role = 'ADMIN'
0
530
And request
0
[
{
"tierType": "GENERAL",
"price": 55.00,
"quota": 50
}
]
540
When method post
10
22:53:21.576 request:
3 > POST http://localhost:8081/api/v1/events/07cf61a8-5dfb-476d-98da-0b6c7adadeb0/tiers
3 > X-Role: ADMIN
3 > Content-Type: application/json; charset=UTF-8
3 > Content-Length: 48
3 > Host: localhost:8081
3 > Connection: Keep-Alive
3 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
3 > Accept-Encoding: gzip,deflate
[{"tierType":"GENERAL","price":55.0,"quota":50}]
22:53:21.586 response time in milliseconds: 10
3 < 201
3 < Content-Type: application/json
3 < Transfer-Encoding: chunked
3 < Date: Wed, 08 Apr 2026 03:53:21 GMT
3 < Keep-Alive: timeout=60
3 < Connection: keep-alive
{"eventId":"07cf61a8-5dfb-476d-98da-0b6c7adadeb0","tiers":[{"id":"edcc8a8f-9d83-4306-9c67-503c4bc6625e","tierType":"GENERAL","price":55.0,"quota":50,"validFrom":null,"validUntil":null,"createdAt":"2026-04-08T03:53:21.580423234","updatedAt":"2026-04-08T03:53:21.580430081"}]}
541
Then status 201
0
542
* def tierId = response.tiers[0].id || response.tiers[0].tierId || response[0].id || response[0].tierId
0
544
Given url baseUrlEvents + '/api/v1/events/' + eventId + '/publish'
0
545
And header X-Role = 'ADMIN'
0
546
When method patch
12
22:53:21.587 request:
4 > PATCH http://localhost:8081/api/v1/events/07cf61a8-5dfb-476d-98da-0b6c7adadeb0/publish
4 > X-Role: ADMIN
4 > Host: localhost:8081
4 > Connection: Keep-Alive
4 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
4 > Accept-Encoding: gzip,deflate
22:53:21.598 response time in milliseconds: 11
4 < 200
4 < Content-Type: application/json
4 < Transfer-Encoding: chunked
4 < Date: Wed, 08 Apr 2026 03:53:21 GMT
4 < Keep-Alive: timeout=60
4 < Connection: keep-alive
{"id":"07cf61a8-5dfb-476d-98da-0b6c7adadeb0","roomId":"5a873bd7-822e-4199-87fa-4d440825d8e8","title":"Advanced Lifecycle Test 192b4592-25e2-414d-b712-7d42e5643483 - Confirmed Protection","description":"Test confirmed purchase is not released","date":"2026-12-15T20:00:00","capacity":50,"status":"PUBLISHED","createdAt":"2026-04-08T03:53:21.569349","updatedAt":"2026-04-08T03:53:21.591979276","createdBy":"00000000-0000-0000-0000-000000000001","imageUrl":null,"subtitle":null,"location":null,"director":null,"castMembers":null,"duration":null,"tag":null,"isLimited":false,"isFeatured":false,"enableSeats":false,"author":null}
547
Then status 200
0
# Create reservation + pay APPROVED
550
* def buyer3 = UUID.randomUUID() + ''
0
551
* def buyer3Email = 'buyer3-protect-' + buyer3.substring(0,8) + '@karate-test.com'
0
552
Given url baseUrlTicketing + '/api/v1/reservations'
0
553
And header X-User-Id = buyer3
0
554
And request
0
{
"eventId": "#(eventId)",
"tierId": "#(tierId)",
"buyerEmail": "#(buyer3Email)"
}
562
When method post
29
22:53:21.600 request:
5 > POST http://localhost:8082/api/v1/reservations
5 > X-User-Id: d447d2aa-136b-49da-867e-0145e12a2b12
5 > Content-Type: application/json; charset=UTF-8
5 > Content-Length: 153
5 > Host: localhost:8082
5 > Connection: Keep-Alive
5 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
5 > Accept-Encoding: gzip,deflate
{"eventId":"07cf61a8-5dfb-476d-98da-0b6c7adadeb0","tierId":"edcc8a8f-9d83-4306-9c67-503c4bc6625e","buyerEmail":"buyer3-protect-d447d2aa@karate-test.com"}
22:53:21.628 response time in milliseconds: 28
5 < 201
5 < Content-Type: application/json
5 < Transfer-Encoding: chunked
5 < Date: Wed, 08 Apr 2026 03:53:21 GMT
5 < Keep-Alive: timeout=60
5 < Connection: keep-alive
{"id":"ab7793f7-d81d-4551-bd8f-22131e931a01","eventId":"07cf61a8-5dfb-476d-98da-0b6c7adadeb0","tierId":"edcc8a8f-9d83-4306-9c67-503c4bc6625e","buyerId":"d447d2aa-136b-49da-867e-0145e12a2b12","status":"PENDING","createdAt":"2026-04-08T03:53:21.621628743","updatedAt":"2026-04-08T03:53:21.621628743","validUntilAt":"2026-04-08T04:03:21.621628743"}
563
Then status 201
0
564
* def res3 = response.id
0
566
Given url baseUrlTicketing + '/api/v1/reservations/' + res3 + '/payments'
0
567
And header X-User-Id = buyer3
0
568
And request
0
{
"amount": 55.00,
"paymentMethod": "MOCK",
"status": "APPROVED"
}
576
When method post
25
22:53:21.629 request:
6 > POST http://localhost:8082/api/v1/reservations/ab7793f7-d81d-4551-bd8f-22131e931a01/payments
6 > X-User-Id: d447d2aa-136b-49da-867e-0145e12a2b12
6 > Content-Type: application/json; charset=UTF-8
6 > Content-Length: 58
6 > Host: localhost:8082
6 > Connection: Keep-Alive
6 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
6 > Accept-Encoding: gzip,deflate
{"amount":55.0,"paymentMethod":"MOCK","status":"APPROVED"}
22:53:21.654 response time in milliseconds: 25
6 < 200
6 < Content-Type: application/json
6 < Transfer-Encoding: chunked
6 < Date: Wed, 08 Apr 2026 03:53:21 GMT
6 < Keep-Alive: timeout=60
6 < Connection: keep-alive
{"reservationId":"ab7793f7-d81d-4551-bd8f-22131e931a01","status":"CONFIRMED","ticketId":"ccc11541-919a-4644-8686-e3cf406b9cb0","message":"Payment approved. Ticket generated.","ticket":{"ticketId":"ccc11541-919a-4644-8686-e3cf406b9cb0","eventId":"07cf61a8-5dfb-476d-98da-0b6c7adadeb0","eventTitle":"Advanced Lifecycle Test 192b4592-25e2-414d-b712-7d42e5643483 - Confirmed Protection","eventDate":"2026-12-15T20:00:00","tier":"GENERAL","pricePaid":55.0,"status":"VALID","purchasedAt":"2026-04-08T03:53:21.64029245","buyerEmail":"buyer3-protect-d447d2aa@karate-test.com","reservationId":"ab7793f7-d81d-4551-bd8f-22131e931a01"},"timestamp":"2026-04-08T03:53:21.646446435"}
577
Then status 200
0
578
* def ticketId = response.ticketId
0
579
* print 'Reservation confirmed with ticket:', ticketId
0
22:53:21.654 [print] Reservation confirmed with ticket: ccc11541-919a-4644-8686-e3cf406b9cb0
# Verify reservation status is CONFIRMED in DB
582
* def sqlResult = karate.call('classpath:common/sql/db-helper.feature@checkReservationStatus', { reservationId: res3, expectedStatus: 'CONFIRMED' })
132
>>
common.sql.db-helper
128
5
* def dbTicketingUrl = karate.properties['dbTicketingUrl'] || 'jdbc:postgresql://localhost:5434/ticketing_db'
0
6
* def dbTicketingUser = karate.properties['dbTicketingUser'] || 'postgres'
0
7
* def dbTicketingPass = karate.properties['dbTicketingPass'] || 'postgres'
0
9
* def dbEventsUrl = karate.properties['dbEventsUrl'] || 'jdbc:postgresql://localhost:5433/events_db'
0
10
* def dbEventsUser = karate.properties['dbEventsUser'] || 'postgres'
0
11
* def dbEventsPass = karate.properties['dbEventsPass'] || 'postgres'
0
13
* def JClass = Java.type('java.lang.Class')
0
14
* def DriverManager = Java.type('java.sql.DriverManager')
0
15
* JClass.forName('org.postgresql.Driver')
0
32
* def sql = 'SELECT status, updated_at FROM reservation WHERE id = ?::uuid ORDER BY updated_at DESC LIMIT 1'
0
33
* def conn = DriverManager.getConnection(dbTicketingUrl, dbTicketingUser, dbTicketingPass)
123
34
* def pstmt = conn.prepareStatement(sql)
0
35
* pstmt.setObject(1, reservationId)
0
36
* def rs = pstmt.executeQuery()
2
38
* def found = rs.next()
0
39
* eval if (!found) karate.fail('Reservation not found in database for id=' + reservationId)
0
41
* def actualStatus = rs.getString('status')
0
42
* def updatedAt = rs.getString('updated_at')
0
44
* rs.close()
0
45
* pstmt.close()
0
46
* conn.close()
0
48
* print 'SQL Result: reservationId=' + reservationId + ', status=' + actualStatus + ', updated_at=' + updatedAt
0
22:53:21.786 [print] SQL Result: reservationId=ab7793f7-d81d-4551-bd8f-22131e931a01, status=CONFIRMED, updated_at=2026-04-08 03:53:21.646696
49
* eval if (actualStatus != expectedStatus) karate.fail('Expected status ' + expectedStatus + ' but found ' + actualStatus)
0
51
* def response = { actualStatus: actualStatus, updatedAt: updatedAt, passed: true }
0
583
* print 'Reservation confirmed in DB'
0
22:53:21.786 [print] Reservation confirmed in DB
# Wait 90 seconds (scheduler window for expiration)
586
* print 'Waiting 90 seconds (scheduler window)...'
0
22:53:21.787 [print] Waiting 90 seconds (scheduler window)...
587
* java.lang.Thread.sleep(90000)
90001
# Verify reservation is STILL CONFIRMED (not released)
590
* def sqlResult = karate.call('classpath:common/sql/db-helper.feature@checkReservationStatus', { reservationId: res3, expectedStatus: 'CONFIRMED' })
21
>>
common.sql.db-helper
11
5
* def dbTicketingUrl = karate.properties['dbTicketingUrl'] || 'jdbc:postgresql://localhost:5434/ticketing_db'
0
6
* def dbTicketingUser = karate.properties['dbTicketingUser'] || 'postgres'
0
7
* def dbTicketingPass = karate.properties['dbTicketingPass'] || 'postgres'
0
9
* def dbEventsUrl = karate.properties['dbEventsUrl'] || 'jdbc:postgresql://localhost:5433/events_db'
0
10
* def dbEventsUser = karate.properties['dbEventsUser'] || 'postgres'
0
11
* def dbEventsPass = karate.properties['dbEventsPass'] || 'postgres'
0
13
* def JClass = Java.type('java.lang.Class')
0
14
* def DriverManager = Java.type('java.sql.DriverManager')
0
15
* JClass.forName('org.postgresql.Driver')
0
32
* def sql = 'SELECT status, updated_at FROM reservation WHERE id = ?::uuid ORDER BY updated_at DESC LIMIT 1'
0
33
* def conn = DriverManager.getConnection(dbTicketingUrl, dbTicketingUser, dbTicketingPass)
7
34
* def pstmt = conn.prepareStatement(sql)
0
35
* pstmt.setObject(1, reservationId)
0
36
* def rs = pstmt.executeQuery()
2
38
* def found = rs.next()
0
39
* eval if (!found) karate.fail('Reservation not found in database for id=' + reservationId)
0
41
* def actualStatus = rs.getString('status')
0
42
* def updatedAt = rs.getString('updated_at')
0
44
* rs.close()
0
45
* pstmt.close()
0
46
* conn.close()
0
48
* print 'SQL Result: reservationId=' + reservationId + ', status=' + actualStatus + ', updated_at=' + updatedAt
0
22:54:51.808 [print] SQL Result: reservationId=ab7793f7-d81d-4551-bd8f-22131e931a01, status=CONFIRMED, updated_at=2026-04-08 03:53:21.646696
49
* eval if (actualStatus != expectedStatus) karate.fail('Expected status ' + expectedStatus + ' but found ' + actualStatus)
0
51
* def response = { actualStatus: actualStatus, updatedAt: updatedAt, passed: true }
0
591
* print 'Reservation still CONFIRMED after scheduler window'
0
22:54:51.809 [print] Reservation still CONFIRMED after scheduler window
# Verify tier still shows quota decremented by 1 (not released)
594
Given url baseUrlEvents + '/api/v1/events/' + eventId + '/tiers'
0
595
When method get
5
22:54:51.809 request:
7 > GET http://localhost:8081/api/v1/events/07cf61a8-5dfb-476d-98da-0b6c7adadeb0/tiers
7 > Host: localhost:8081
7 > Connection: Keep-Alive
7 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
7 > Accept-Encoding: gzip,deflate
22:54:51.814 response time in milliseconds: 5
7 < 200
7 < Content-Type: application/json
7 < Transfer-Encoding: chunked
7 < Date: Wed, 08 Apr 2026 03:54:51 GMT
7 < Keep-Alive: timeout=60
7 < Connection: keep-alive
{"eventId":"07cf61a8-5dfb-476d-98da-0b6c7adadeb0","tiers":[{"id":"edcc8a8f-9d83-4306-9c67-503c4bc6625e","tierType":"GENERAL","price":55.00,"quota":49,"validFrom":null,"validUntil":null,"createdAt":"2026-04-08T03:53:21.580423","updatedAt":"2026-04-08T03:53:21.614357"}]}
596
Then status 200
0
597
* def tierProtected = response.tiers[0]
0
598
* print 'Tier after scheduler window: quota=' + tierProtected.quota
1
22:54:51.815 [print] Tier after scheduler window: quota=49
599
* assert tierProtected.quota == 49
0
600
* print '✅ Scenario 4 PASS: Confirmed purchase protected (NOT released by scheduler)'
0
22:54:51.815 [print] ✅ Scenario 4 PASS: Confirmed purchase protected (NOT released by scheduler)
Scenario: [6:603]
Scenario 5 - Backup Job / Fallback Mechanism (if exposed by runtime)
ms: 307
>>
Background:
7
* def baseUrlEvents = karate.properties['baseUrlEvents'] || 'http://localhost:8081'
0
22:54:51.817 karate.env system property was: null
8
* def baseUrlTicketing = karate.properties['baseUrlTicketing'] || 'http://localhost:8082'
0
9
* def UUID = Java.type('java.util.UUID')
0
10
* def eventTitle = 'Advanced Lifecycle Test ' + UUID.randomUUID() + ''
0
11
* def futureDate = '2026-12-15T20:00:00'
0
# Setup: Create room
606
Given url baseUrlEvents + '/api/v1/rooms'
0
607
And header X-Role = 'ADMIN'
0
608
And header X-User-Id = '00000000-0000-0000-0000-000000000001'
0
609
And request
0
{
"name": "Backup Job Test Room",
"maxCapacity": 100
}
616
When method post
13
22:54:51.818 request:
1 > POST http://localhost:8081/api/v1/rooms
1 > X-Role: ADMIN
1 > X-User-Id: 00000000-0000-0000-0000-000000000001
1 > Content-Type: application/json; charset=UTF-8
1 > Content-Length: 49
1 > Host: localhost:8081
1 > Connection: Keep-Alive
1 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
1 > Accept-Encoding: gzip,deflate
{"name":"Backup Job Test Room","maxCapacity":100}
22:54:51.830 response time in milliseconds: 12
1 < 201
1 < Content-Type: application/json
1 < Transfer-Encoding: chunked
1 < Date: Wed, 08 Apr 2026 03:54:51 GMT
1 < Keep-Alive: timeout=60
1 < Connection: keep-alive
{"id":"b969a7d1-f379-4587-a35b-e083046f712c","name":"Backup Job Test Room","maxCapacity":100,"created_at":"2026-04-08T03:54:51.820416678","updated_at":"2026-04-08T03:54:51.820424891"}
617
Then status 201
0
618
* def roomId = response.id ? response.id : response.roomId
0
# Setup: Create event + tier + publish
621
* def scenarioTitle = eventTitle + ' - Backup Job Test'
0
622
Given url baseUrlEvents + '/api/v1/events'
0
623
And header X-Role = 'ADMIN'
0
624
And header X-User-Id = '00000000-0000-0000-0000-000000000001'
0
625
And request
0
{
"roomId": "#(roomId)",
"title": "#(scenarioTitle)",
"description": "Test backup job if available",
"date": "#(futureDate)",
"capacity": 30,
"enableSeats": false
}
636
When method post
14
22:54:51.831 request:
2 > POST http://localhost:8081/api/v1/events
2 > X-Role: ADMIN
2 > X-User-Id: 00000000-0000-0000-0000-000000000001
2 > Content-Type: application/json; charset=UTF-8
2 > Content-Length: 246
2 > Host: localhost:8081
2 > Connection: Keep-Alive
2 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
2 > Accept-Encoding: gzip,deflate
{"roomId":"b969a7d1-f379-4587-a35b-e083046f712c","title":"Advanced Lifecycle Test 9b6a9cc2-dd5c-4535-91af-291e0880b58b - Backup Job Test","description":"Test backup job if available","date":"2026-12-15T20:00:00","capacity":30,"enableSeats":false}
22:54:51.844 response time in milliseconds: 13
2 < 201
2 < Content-Type: application/json
2 < Transfer-Encoding: chunked
2 < Date: Wed, 08 Apr 2026 03:54:51 GMT
2 < Keep-Alive: timeout=60
2 < Connection: keep-alive
{"id":"99a3924c-af20-44a1-b91d-fafac9ac538f","roomId":"b969a7d1-f379-4587-a35b-e083046f712c","title":"Advanced Lifecycle Test 9b6a9cc2-dd5c-4535-91af-291e0880b58b - Backup Job Test","description":"Test backup job if available","date":"2026-12-15T20:00:00","capacity":30,"status":"DRAFT","createdAt":"2026-04-08T03:54:51.835561422","updatedAt":"2026-04-08T03:54:51.835566243","createdBy":"00000000-0000-0000-0000-000000000001","imageUrl":null,"subtitle":null,"location":null,"director":null,"castMembers":null,"duration":null,"tag":null,"isLimited":false,"isFeatured":false,"enableSeats":false,"author":null}
637
Then status 201
0
638
* def eventId = response.id ? response.id : response.eventId
0
640
Given url baseUrlEvents + '/api/v1/events/' + eventId + '/tiers'
0
641
And header X-Role = 'ADMIN'
0
642
And request
0
[
{
"tierType": "GENERAL",
"price": 80.00,
"quota": 30
}
]
652
When method post
13
22:54:51.846 request:
3 > POST http://localhost:8081/api/v1/events/99a3924c-af20-44a1-b91d-fafac9ac538f/tiers
3 > X-Role: ADMIN
3 > Content-Type: application/json; charset=UTF-8
3 > Content-Length: 48
3 > Host: localhost:8081
3 > Connection: Keep-Alive
3 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
3 > Accept-Encoding: gzip,deflate
[{"tierType":"GENERAL","price":80.0,"quota":30}]
22:54:51.858 response time in milliseconds: 11
3 < 201
3 < Content-Type: application/json
3 < Transfer-Encoding: chunked
3 < Date: Wed, 08 Apr 2026 03:54:51 GMT
3 < Keep-Alive: timeout=60
3 < Connection: keep-alive
{"eventId":"99a3924c-af20-44a1-b91d-fafac9ac538f","tiers":[{"id":"03b11904-cd01-46b1-b854-e8746fa0dfca","tierType":"GENERAL","price":80.0,"quota":30,"validFrom":null,"validUntil":null,"createdAt":"2026-04-08T03:54:51.851403591","updatedAt":"2026-04-08T03:54:51.851416551"}]}
653
Then status 201
0
654
* def tierId = response.tiers[0].id || response.tiers[0].tierId || response[0].id || response[0].tierId
0
656
Given url baseUrlEvents + '/api/v1/events/' + eventId + '/publish'
0
657
And header X-Role = 'ADMIN'
0
658
When method patch
11
22:54:51.859 request:
4 > PATCH http://localhost:8081/api/v1/events/99a3924c-af20-44a1-b91d-fafac9ac538f/publish
4 > X-Role: ADMIN
4 > Host: localhost:8081
4 > Connection: Keep-Alive
4 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
4 > Accept-Encoding: gzip,deflate
22:54:51.869 response time in milliseconds: 10
4 < 200
4 < Content-Type: application/json
4 < Transfer-Encoding: chunked
4 < Date: Wed, 08 Apr 2026 03:54:51 GMT
4 < Keep-Alive: timeout=60
4 < Connection: keep-alive
{"id":"99a3924c-af20-44a1-b91d-fafac9ac538f","roomId":"b969a7d1-f379-4587-a35b-e083046f712c","title":"Advanced Lifecycle Test 9b6a9cc2-dd5c-4535-91af-291e0880b58b - Backup Job Test","description":"Test backup job if available","date":"2026-12-15T20:00:00","capacity":30,"status":"PUBLISHED","createdAt":"2026-04-08T03:54:51.835561","updatedAt":"2026-04-08T03:54:51.86242416","createdBy":"00000000-0000-0000-0000-000000000001","imageUrl":null,"subtitle":null,"location":null,"director":null,"castMembers":null,"duration":null,"tag":null,"isLimited":false,"isFeatured":false,"enableSeats":false,"author":null}
659
Then status 200
0
# Create 3 reservations: 1 will expire, 1 will be confirmed, 1 will stay pending
662
* def buyerA = UUID.randomUUID() + ''
0
663
* def buyerAEmail = 'buyerA-backup-' + buyerA.substring(0,8) + '@karate-test.com'
0
664
Given url baseUrlTicketing + '/api/v1/reservations'
0
665
And header X-User-Id = buyerA
0
666
And request
0
{
"eventId": "#(eventId)",
"tierId": "#(tierId)",
"buyerEmail": "#(buyerAEmail)"
}
674
When method post
34
22:54:51.871 request:
5 > POST http://localhost:8082/api/v1/reservations
5 > X-User-Id: 0e3c0363-c240-4fc7-a23e-9a56f0f4e2bc
5 > Content-Type: application/json; charset=UTF-8
5 > Content-Length: 152
5 > Host: localhost:8082
5 > Connection: Keep-Alive
5 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
5 > Accept-Encoding: gzip,deflate
{"eventId":"99a3924c-af20-44a1-b91d-fafac9ac538f","tierId":"03b11904-cd01-46b1-b854-e8746fa0dfca","buyerEmail":"buyerA-backup-0e3c0363@karate-test.com"}
22:54:51.904 response time in milliseconds: 33
5 < 201
5 < Content-Type: application/json
5 < Transfer-Encoding: chunked
5 < Date: Wed, 08 Apr 2026 03:54:51 GMT
5 < Keep-Alive: timeout=60
5 < Connection: keep-alive
{"id":"09358080-4cb4-4962-82e8-92fa717dde7b","eventId":"99a3924c-af20-44a1-b91d-fafac9ac538f","tierId":"03b11904-cd01-46b1-b854-e8746fa0dfca","buyerId":"0e3c0363-c240-4fc7-a23e-9a56f0f4e2bc","status":"PENDING","createdAt":"2026-04-08T03:54:51.895737052","updatedAt":"2026-04-08T03:54:51.895737052","validUntilAt":"2026-04-08T04:04:51.895737052"}
675
Then status 201
0
676
* def resA = response.id
0
678
* def buyerB = UUID.randomUUID() + ''
0
679
* def buyerBEmail = 'buyerB-backup-' + buyerB.substring(0,8) + '@karate-test.com'
0
680
Given url baseUrlTicketing + '/api/v1/reservations'
0
681
And header X-User-Id = buyerB
0
682
And request
0
{
"eventId": "#(eventId)",
"tierId": "#(tierId)",
"buyerEmail": "#(buyerBEmail)"
}
690
When method post
27
22:54:51.906 request:
6 > POST http://localhost:8082/api/v1/reservations
6 > X-User-Id: 9a739270-798f-426e-a566-ac1521805762
6 > Content-Type: application/json; charset=UTF-8
6 > Content-Length: 152
6 > Host: localhost:8082
6 > Connection: Keep-Alive
6 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
6 > Accept-Encoding: gzip,deflate
{"eventId":"99a3924c-af20-44a1-b91d-fafac9ac538f","tierId":"03b11904-cd01-46b1-b854-e8746fa0dfca","buyerEmail":"buyerB-backup-9a739270@karate-test.com"}
22:54:51.932 response time in milliseconds: 26
6 < 201
6 < Content-Type: application/json
6 < Transfer-Encoding: chunked
6 < Date: Wed, 08 Apr 2026 03:54:51 GMT
6 < Keep-Alive: timeout=60
6 < Connection: keep-alive
{"id":"cfc6db84-548e-42e4-af44-e7c3950404d8","eventId":"99a3924c-af20-44a1-b91d-fafac9ac538f","tierId":"03b11904-cd01-46b1-b854-e8746fa0dfca","buyerId":"9a739270-798f-426e-a566-ac1521805762","status":"PENDING","createdAt":"2026-04-08T03:54:51.926032881","updatedAt":"2026-04-08T03:54:51.926032881","validUntilAt":"2026-04-08T04:04:51.926032881"}
691
Then status 201
0
692
* def resB = response.id
0
694
* def buyerC = UUID.randomUUID() + ''
0
695
* def buyerCEmail = 'buyerC-backup-' + buyerC.substring(0,8) + '@karate-test.com'
0
696
Given url baseUrlTicketing + '/api/v1/reservations'
0
697
And header X-User-Id = buyerC
0
698
And request
0
{
"eventId": "#(eventId)",
"tierId": "#(tierId)",
"buyerEmail": "#(buyerCEmail)"
}
706
When method post
28
22:54:51.934 request:
7 > POST http://localhost:8082/api/v1/reservations
7 > X-User-Id: fae25068-689a-442b-b4c0-5662047bacd6
7 > Content-Type: application/json; charset=UTF-8
7 > Content-Length: 152
7 > Host: localhost:8082
7 > Connection: Keep-Alive
7 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
7 > Accept-Encoding: gzip,deflate
{"eventId":"99a3924c-af20-44a1-b91d-fafac9ac538f","tierId":"03b11904-cd01-46b1-b854-e8746fa0dfca","buyerEmail":"buyerC-backup-fae25068@karate-test.com"}
22:54:51.961 response time in milliseconds: 27
7 < 201
7 < Content-Type: application/json
7 < Transfer-Encoding: chunked
7 < Date: Wed, 08 Apr 2026 03:54:51 GMT
7 < Keep-Alive: timeout=60
7 < Connection: keep-alive
{"id":"f404b31f-467c-4da1-8bcc-cbf74d854ef8","eventId":"99a3924c-af20-44a1-b91d-fafac9ac538f","tierId":"03b11904-cd01-46b1-b854-e8746fa0dfca","buyerId":"fae25068-689a-442b-b4c0-5662047bacd6","status":"PENDING","createdAt":"2026-04-08T03:54:51.954680766","updatedAt":"2026-04-08T03:54:51.954680766","validUntilAt":"2026-04-08T04:04:51.954680766"}
707
Then status 201
0
708
* def resC = response.id
0
# Pay resB -> CONFIRMED (should survive)
711
Given url baseUrlTicketing + '/api/v1/reservations/' + resB + '/payments'
0
712
And header X-User-Id = buyerB
0
713
And request
0
{
"amount": 80.00,
"paymentMethod": "MOCK",
"status": "APPROVED"
}
721
When method post
28
22:54:51.963 request:
8 > POST http://localhost:8082/api/v1/reservations/cfc6db84-548e-42e4-af44-e7c3950404d8/payments
8 > X-User-Id: 9a739270-798f-426e-a566-ac1521805762
8 > Content-Type: application/json; charset=UTF-8
8 > Content-Length: 58
8 > Host: localhost:8082
8 > Connection: Keep-Alive
8 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
8 > Accept-Encoding: gzip,deflate
{"amount":80.0,"paymentMethod":"MOCK","status":"APPROVED"}
22:54:51.989 response time in milliseconds: 26
8 < 200
8 < Content-Type: application/json
8 < Transfer-Encoding: chunked
8 < Date: Wed, 08 Apr 2026 03:54:51 GMT
8 < Keep-Alive: timeout=60
8 < Connection: keep-alive
{"reservationId":"cfc6db84-548e-42e4-af44-e7c3950404d8","status":"CONFIRMED","ticketId":"502a6440-9c70-4075-9531-d3447a0b5c78","message":"Payment approved. Ticket generated.","ticket":{"ticketId":"502a6440-9c70-4075-9531-d3447a0b5c78","eventId":"99a3924c-af20-44a1-b91d-fafac9ac538f","eventTitle":"Advanced Lifecycle Test 9b6a9cc2-dd5c-4535-91af-291e0880b58b - Backup Job Test","eventDate":"2026-12-15T20:00:00","tier":"GENERAL","pricePaid":80.0,"status":"VALID","purchasedAt":"2026-04-08T03:54:51.974797214","buyerEmail":"buyerB-backup-9a739270@karate-test.com","reservationId":"cfc6db84-548e-42e4-af44-e7c3950404d8"},"timestamp":"2026-04-08T03:54:51.981234752"}
722
Then status 200
0
# Force resA to expire directly by DB helper (bypassing normal flow) to simulate an orphaned reservation
725
* def sqlResult = karate.call('classpath:common/sql/db-helper.feature@forceExpiration', { reservationId: resA })
19
>>
common.sql.db-helper
14
5
* def dbTicketingUrl = karate.properties['dbTicketingUrl'] || 'jdbc:postgresql://localhost:5434/ticketing_db'
0
6
* def dbTicketingUser = karate.properties['dbTicketingUser'] || 'postgres'
0
7
* def dbTicketingPass = karate.properties['dbTicketingPass'] || 'postgres'
0
9
* def dbEventsUrl = karate.properties['dbEventsUrl'] || 'jdbc:postgresql://localhost:5433/events_db'
0
10
* def dbEventsUser = karate.properties['dbEventsUser'] || 'postgres'
0
11
* def dbEventsPass = karate.properties['dbEventsPass'] || 'postgres'
0
13
* def JClass = Java.type('java.lang.Class')
0
14
* def DriverManager = Java.type('java.sql.DriverManager')
0
15
* JClass.forName('org.postgresql.Driver')
0
19
* def sql = "UPDATE reservation SET valid_until_at = NOW() AT TIME ZONE 'UTC' - INTERVAL '2 days' WHERE id = ?::uuid"
0
20
* def conn = DriverManager.getConnection(dbTicketingUrl, dbTicketingUser, dbTicketingPass)
6
21
* def pstmt = conn.prepareStatement(sql)
0
22
* pstmt.setObject(1, reservationId)
0
23
* def rows = pstmt.executeUpdate()
7
24
* pstmt.close()
0
25
* conn.close()
0
26
* print 'SQL Result: Forced expiration for reservationId=' + reservationId + ', Rows updated=' + rows
0
22:54:52.008 [print] SQL Result: Forced expiration for reservationId=09358080-4cb4-4962-82e8-92fa717dde7b, Rows updated=1
27
* eval if (rows != 1) karate.fail('Expected exactly 1 reservation row to be updated, but updated ' + rows)
0
28
* def response = { rowsUpdated: rows, passed: true }
0
# resC stays PENDING naturally. We simulate passing of time (e.g. 25 minutes)
728
Given url baseUrlTicketing + '/api/v1/testability/clock/advance'
0
729
And param minutes = 25
0
730
When method post
3
22:54:52.010 request:
9 > POST http://localhost:8082/api/v1/testability/clock/advance?minutes=25
9 > Host: localhost:8082
9 > Connection: Keep-Alive
9 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
9 > Accept-Encoding: gzip,deflate
22:54:52.012 response time in milliseconds: 2
9 < 200
9 < Content-Type: application/json
9 < Transfer-Encoding: chunked
9 < Date: Wed, 08 Apr 2026 03:54:51 GMT
9 < Keep-Alive: timeout=60
9 < Connection: keep-alive
{"status":"advanced","current_time":"2026-04-08T04:19:52.011943305"}
731
Then status 200
0
# Act: Trigger Backup Schedule Explicitly
734
* print 'Triggering Backup Job Manually via Testability Endpoint to clean up lingering objects...'
0
22:54:52.013 [print] Triggering Backup Job Manually via Testability Endpoint to clean up lingering objects...
735
Given url baseUrlTicketing + '/api/v1/testability/jobs/expiration/trigger'
0
736
When method post
79
22:54:52.014 request:
10 > POST http://localhost:8082/api/v1/testability/jobs/expiration/trigger
10 > Host: localhost:8082
10 > Connection: Keep-Alive
10 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
10 > Accept-Encoding: gzip,deflate
22:54:52.092 response time in milliseconds: 78
10 < 200
10 < Content-Type: application/json
10 < Transfer-Encoding: chunked
10 < Date: Wed, 08 Apr 2026 03:54:51 GMT
10 < Keep-Alive: timeout=60
10 < Connection: keep-alive
{"status":"Triggered processExpiredBatch"}
737
Then status 200
0
# Verify states directly via API:
# - resA: EXPIRED (cleaned by backup job)
# - resB: CONFIRMED (protected)
# - resC: EXPIRED (time-based expiration cleaned by job)
744
Given url baseUrlTicketing + '/api/v1/reservations/' + resA
0
745
And header X-User-Id = buyerA
0
746
When method get
6
22:54:52.094 request:
11 > GET http://localhost:8082/api/v1/reservations/09358080-4cb4-4962-82e8-92fa717dde7b
11 > X-User-Id: 0e3c0363-c240-4fc7-a23e-9a56f0f4e2bc
11 > Host: localhost:8082
11 > Connection: Keep-Alive
11 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
11 > Accept-Encoding: gzip,deflate
22:54:52.099 response time in milliseconds: 5
11 < 200
11 < Content-Type: application/json
11 < Transfer-Encoding: chunked
11 < Date: Wed, 08 Apr 2026 03:54:51 GMT
11 < Keep-Alive: timeout=60
11 < Connection: keep-alive
{"id":"09358080-4cb4-4962-82e8-92fa717dde7b","eventId":"99a3924c-af20-44a1-b91d-fafac9ac538f","tierId":"03b11904-cd01-46b1-b854-e8746fa0dfca","status":"EXPIRED","createdAt":"2026-04-08T03:54:51.895737","updatedAt":"2026-04-08T03:54:52.081574","validUntilAt":"2026-04-06T03:54:52.001734"}
747
Then status 200
0
748
* match response.status == 'EXPIRED'
0
749
* print 'resA: Purged by backup job (EXPIRED)'
0
22:54:52.100 [print] resA: Purged by backup job (EXPIRED)
751
Given url baseUrlTicketing + '/api/v1/reservations/' + resB
0
752
And header X-User-Id = buyerB
0
753
When method get
9
22:54:52.102 request:
12 > GET http://localhost:8082/api/v1/reservations/cfc6db84-548e-42e4-af44-e7c3950404d8
12 > X-User-Id: 9a739270-798f-426e-a566-ac1521805762
12 > Host: localhost:8082
12 > Connection: Keep-Alive
12 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
12 > Accept-Encoding: gzip,deflate
22:54:52.110 response time in milliseconds: 8
12 < 200
12 < Content-Type: application/json
12 < Transfer-Encoding: chunked
12 < Date: Wed, 08 Apr 2026 03:54:51 GMT
12 < Keep-Alive: timeout=60
12 < Connection: keep-alive
{"id":"cfc6db84-548e-42e4-af44-e7c3950404d8","eventId":"99a3924c-af20-44a1-b91d-fafac9ac538f","tierId":"03b11904-cd01-46b1-b854-e8746fa0dfca","status":"CONFIRMED","createdAt":"2026-04-08T03:54:51.926033","updatedAt":"2026-04-08T03:54:51.981464","validUntilAt":"2026-04-08T04:04:51.926033"}
754
Then status 200
0
755
* match response.status == 'CONFIRMED'
0
756
* print 'resB: CONFIRMED (protected)'
0
22:54:52.111 [print] resB: CONFIRMED (protected)
758
Given url baseUrlTicketing + '/api/v1/reservations/' + resC
0
759
And header X-User-Id = buyerC
0
760
When method get
8
22:54:52.112 request:
13 > GET http://localhost:8082/api/v1/reservations/f404b31f-467c-4da1-8bcc-cbf74d854ef8
13 > X-User-Id: fae25068-689a-442b-b4c0-5662047bacd6
13 > Host: localhost:8082
13 > Connection: Keep-Alive
13 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
13 > Accept-Encoding: gzip,deflate
22:54:52.119 response time in milliseconds: 7
13 < 200
13 < Content-Type: application/json
13 < Transfer-Encoding: chunked
13 < Date: Wed, 08 Apr 2026 03:54:51 GMT
13 < Keep-Alive: timeout=60
13 < Connection: keep-alive
{"id":"f404b31f-467c-4da1-8bcc-cbf74d854ef8","eventId":"99a3924c-af20-44a1-b91d-fafac9ac538f","tierId":"03b11904-cd01-46b1-b854-e8746fa0dfca","status":"EXPIRED","createdAt":"2026-04-08T03:54:51.954681","updatedAt":"2026-04-08T03:54:52.060661","validUntilAt":"2026-04-08T04:04:51.954681"}
761
Then status 200
0
762
* match response.status == 'EXPIRED'
0
763
* print 'resC: Purged by backup job (EXPIRED)'
0
22:54:52.120 [print] resC: Purged by backup job (EXPIRED)
# Cleanup Clock
766
Given url baseUrlTicketing + '/api/v1/testability/clock/reset'
0
767
When method post
5
22:54:52.122 request:
14 > POST http://localhost:8082/api/v1/testability/clock/reset
14 > Host: localhost:8082
14 > Connection: Keep-Alive
14 > User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.17)
14 > Accept-Encoding: gzip,deflate
22:54:52.125 response time in milliseconds: 3
14 < 200
14 < Content-Type: application/json
14 < Transfer-Encoding: chunked
14 < Date: Wed, 08 Apr 2026 03:54:51 GMT
14 < Keep-Alive: timeout=60
14 < Connection: keep-alive
{"status":"reset","current_time":"2026-04-08T03:54:52.123910152"}
768
Then status 200
0
770
* print '✅ TC-019 Scenario 5 PASS: Backup job correctly regularized lingering pending reservations'
1
22:54:52.125 [print] ✅ TC-019 Scenario 5 PASS: Backup job correctly regularized lingering pending reservations