Skip to content

Commit d33fe2a

Browse files
committed
#7, implement revoke puzzle command
1 parent 753c576 commit d33fe2a

File tree

10 files changed

+175
-19
lines changed

10 files changed

+175
-19
lines changed

api/command/deliverPuzzle.ts

+8-7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@ import { Attendee } from '@/attendee'
33
import { Booth } from '@/event'
44
import { Status, Config, Stats } from '@/puzzle'
55
import { FindBoothByTokenInput } from '@api/projection'
6+
import {
7+
PuzzleReceiverNotFoundError,
8+
PuzzleDelivererNotFoundError,
9+
PuzzledAlreadyDeliveredError,
10+
PuzzleAttendeeNotInEventError,
11+
PuzzleConfigNotFoundError,
12+
PuzzleStatsNotFoundError,
13+
} from './errors'
614

715
export type DeliverPuzzleInput = {
816
token: string
@@ -15,13 +23,6 @@ export type DeliverPuzzleOutput = {
1523
attendeeName: string
1624
}
1725

18-
export class PuzzleReceiverNotFoundError extends Error {}
19-
export class PuzzleDelivererNotFoundError extends Error {}
20-
export class PuzzledAlreadyDeliveredError extends Error {}
21-
export class PuzzleAttendeeNotInEventError extends Error {}
22-
export class PuzzleConfigNotFoundError extends Error {}
23-
export class PuzzleStatsNotFoundError extends Error {}
24-
2526
export class DeliverPuzzleCommand implements Command<DeliverPuzzleInput, DeliverPuzzleOutput> {
2627
private readonly attendees: Repository<Attendee>
2728
private readonly booths: Projection<FindBoothByTokenInput, Booth>

api/command/errors.ts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export class PuzzleReceiverNotFoundError extends Error {}
2+
export class PuzzleDelivererNotFoundError extends Error {}
3+
export class PuzzledAlreadyDeliveredError extends Error {}
4+
export class PuzzleAttendeeNotInEventError extends Error {}
5+
export class PuzzleConfigNotFoundError extends Error {}
6+
export class PuzzleStatsNotFoundError extends Error {}

api/command/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from './errors'
12
export * from './createAnnouncement'
23
export * from './runAttendeeScenario'
34
export * from './initializeAttendee'

api/command/revokePuzzle.ts

+36-3
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,48 @@
1-
import { Command } from '@/core'
1+
import { Repository, Command } from '@/core'
2+
import { Status, Stats } from '@/puzzle'
3+
import { PuzzleReceiverNotFoundError, PuzzleStatsNotFoundError } from './errors'
24

35
export type RevokePuzzleInput = {
4-
attendeeToken: string
6+
token: string
7+
eventId: string
58
}
69

710
export type RevokePuzzleOutput = {
811
success: boolean
912
}
1013

1114
export class RevokePuzzleCommand implements Command<RevokePuzzleInput, RevokePuzzleOutput> {
12-
async execute(_input: RevokePuzzleInput): Promise<RevokePuzzleOutput> {
15+
private readonly statuses: Repository<Status>
16+
private readonly stats: Repository<Stats>
17+
18+
constructor(statuses: Repository<Status>, stats: Repository<Stats>) {
19+
this.statuses = statuses
20+
this.stats = stats
21+
}
22+
23+
async execute(input: RevokePuzzleInput): Promise<RevokePuzzleOutput> {
24+
const status = await this.statuses.findById(input.token)
25+
if (!status) {
26+
throw new PuzzleReceiverNotFoundError()
27+
}
28+
29+
if (status.isNew()) {
30+
return { success: false }
31+
}
32+
33+
const stats = await this.stats.findById(input.eventId)
34+
if (!stats) {
35+
throw new PuzzleStatsNotFoundError()
36+
}
37+
38+
status.revoke()
39+
for (const piece of status.pieces) {
40+
stats.revokePuzzle(piece.name)
41+
}
42+
43+
await this.statuses.save(status)
44+
await this.stats.save(stats)
45+
1346
return {
1447
success: true,
1548
}

api/repository/puzzleStatuses.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { type D1Database } from '@cloudflare/workers-types'
22
import { type Class } from '@/core/utils'
33
import { Repository } from '@/core'
4-
import { Status, ActivityEvent, AttendeeInitialized, PuzzleCollected } from '@/puzzle'
4+
import { Status, ActivityEvent, AttendeeInitialized, PuzzleCollected, Revoked } from '@/puzzle'
55

66
type EventSchema = {
77
id: string
@@ -15,6 +15,7 @@ type EventSchema = {
1515
const eventConstructors: Record<string, Class<ActivityEvent>> = {
1616
AttendeeInitialized: AttendeeInitialized,
1717
PuzzleCollected: PuzzleCollected,
18+
Revoked: Revoked,
1819
}
1920

2021
export class D1PuzzleStatusRepository implements Repository<Status> {

features/puzzle_revoke.feature

+96-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,26 @@
11
Feature: Puzzle Revoke
22
Scenario: PUT /event/puzzle/revoke to revoke attendee puzzle
3-
Given there have some attendees
3+
Given there have some booths
4+
| token | name | event_id |
5+
| 1024914b-ee65-4728-b687-8341f5affa89 | COSCUP | SITCON |
6+
And event "SITCON" have a puzzle config
7+
"""
8+
{
9+
"pieces": {
10+
"=": 1
11+
}
12+
}
13+
"""
14+
And there have some attendees
415
| token | event_id | display_name |
516
| f185f505-d8c0-43ce-9e7b-bb9e8909072d | SITCON | Aotoki |
6-
When I make a PUT request to "/event/puzzle/revoke?token=f185f505-d8c0-43ce-9e7b-bb9e8909072d":
17+
When I make a POST request to "/event/puzzle/deliver?token=1024914b-ee65-4728-b687-8341f5affa89&event_id=SITCON":
18+
"""
19+
{
20+
"receiver": "f185f505-d8c0-43ce-9e7b-bb9e8909072d"
21+
}
22+
"""
23+
And I make a PUT request to "/event/puzzle/revoke?token=f185f505-d8c0-43ce-9e7b-bb9e8909072d&event_id=SITCON":
724
"""
825
{}
926
"""
@@ -14,3 +31,80 @@ Feature: Puzzle Revoke
1431
}
1532
"""
1633
And the response status should be 200
34+
35+
Scenario: PUT /event/puzzle/revoke to revoke attendee can see revoked puzzle
36+
Given there have some booths
37+
| token | name | event_id |
38+
| 1024914b-ee65-4728-b687-8341f5affa89 | COSCUP | SITCON |
39+
And event "SITCON" have a puzzle config
40+
"""
41+
{
42+
"pieces": {
43+
"=": 1
44+
}
45+
}
46+
"""
47+
And there have some attendees
48+
| token | event_id | display_name |
49+
| f185f505-d8c0-43ce-9e7b-bb9e8909072d | SITCON | Aotoki |
50+
When I make a POST request to "/event/puzzle/deliver?token=1024914b-ee65-4728-b687-8341f5affa89&event_id=SITCON":
51+
"""
52+
{
53+
"receiver": "f185f505-d8c0-43ce-9e7b-bb9e8909072d"
54+
}
55+
"""
56+
And I make a PUT request to "/event/puzzle/revoke?token=f185f505-d8c0-43ce-9e7b-bb9e8909072d&event_id=SITCON":
57+
"""
58+
{}
59+
"""
60+
And I make a GET request to "/event/puzzle?token=f185f505-d8c0-43ce-9e7b-bb9e8909072d&event_id=SITCON"
61+
Then the response json should be:
62+
"""
63+
{
64+
"user_id": "Aotoki",
65+
"puzzles": ["="],
66+
"deliverers": ["COSCUP"],
67+
"valid": 1693065600,
68+
"coupon": 0
69+
}
70+
"""
71+
And the response status should be 200
72+
73+
Scenario: PUT /event/puzzle/revoke to revoke attendee can see stats changed
74+
Given there have some booths
75+
| token | name | event_id |
76+
| 1024914b-ee65-4728-b687-8341f5affa89 | COSCUP | SITCON |
77+
And event "SITCON" have a puzzle config
78+
"""
79+
{
80+
"pieces": {
81+
"=": 1
82+
}
83+
}
84+
"""
85+
And there have some attendees
86+
| token | event_id | display_name |
87+
| f185f505-d8c0-43ce-9e7b-bb9e8909072d | SITCON | Aotoki |
88+
When I make a POST request to "/event/puzzle/deliver?token=1024914b-ee65-4728-b687-8341f5affa89&event_id=SITCON":
89+
"""
90+
{
91+
"receiver": "f185f505-d8c0-43ce-9e7b-bb9e8909072d"
92+
}
93+
"""
94+
And I make a PUT request to "/event/puzzle/revoke?token=f185f505-d8c0-43ce-9e7b-bb9e8909072d&event_id=SITCON":
95+
"""
96+
{}
97+
"""
98+
And I make a GET request to "/event/puzzle/dashboard?event_id=SITCON"
99+
Then the response json should be:
100+
"""
101+
[
102+
{
103+
"puzzle": "=", "quantity": 1, "currency": 0
104+
},
105+
{
106+
"puzzle": "total", "quantity": 1, "currency": 0
107+
}
108+
]
109+
"""
110+
And the response status should be 200

src/puzzle/event.ts

+2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ export class PuzzleCollected extends ActivityEvent {
3232
}
3333
}
3434

35+
export class Revoked extends ActivityEvent {}
36+
3537
export abstract class StatEvent implements DomainEvent {
3638
public readonly id: string
3739
public readonly aggregateId: string

src/puzzle/status.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { AggregateRoot, Replayable, getCurrentTime } from '@/core'
2-
import { ActivityEvent, AttendeeInitialized, PuzzleCollected } from './event'
2+
import { ActivityEvent, AttendeeInitialized, PuzzleCollected, Revoked } from './event'
33
import { Piece } from './piece'
44

55
@Replayable
@@ -42,6 +42,10 @@ export class Status extends AggregateRoot<string, ActivityEvent> {
4242
return [...this._pieces]
4343
}
4444

45+
revoke(): void {
46+
this.apply(new Revoked(crypto.randomUUID(), this.id, getCurrentTime()))
47+
}
48+
4549
collectPiece(name: string, giverName: string): void {
4650
this.apply(new PuzzleCollected(crypto.randomUUID(), this.id, getCurrentTime(), name, giverName))
4751
}
@@ -60,4 +64,12 @@ export class Status extends AggregateRoot<string, ActivityEvent> {
6064
piece.giveBy(giverName, occurredAt)
6165
this._pieces.push(piece)
6266
}
67+
68+
private _onRevoked(event: Revoked) {
69+
this._revokedAt = new Date(event.occurredAt)
70+
71+
if (!this._completedAt) {
72+
this._completedAt = this._revokedAt
73+
}
74+
}
6375
}

worker/controller/revokePuzzle.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export class RevokePuzzle extends OpenAPIRoute {
1616
tags: ['Puzzle'],
1717
requestBody: {},
1818
parameters: {
19+
event_id: schema.EventIdQuery,
1920
token: schema.OptionalAttendeeTokenQuery,
2021
},
2122
responses: {
@@ -27,11 +28,13 @@ export class RevokePuzzle extends OpenAPIRoute {
2728
}
2829

2930
async handle(request: RevokePuzzleRequest, _env: unknown, _context: unknown) {
30-
const input: Command.RevokePuzzleInput = {
31-
attendeeToken: request.query.token as string,
32-
}
31+
const token = request.query.token as string
32+
const eventId = request.query.event_id as string
3333

34-
const output = await request.revokePuzzle.execute(input)
34+
const output = await request.revokePuzzle.execute({
35+
token,
36+
eventId,
37+
})
3538

3639
if (!output.success) {
3740
throw new StatusError(400, 'Unable to revoke puzzle')

worker/middlewares/command.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ export const withCommands = (request: IRequest, env: Env) => {
2727
puzzleConfigRepository,
2828
puzzleStatsRepository
2929
)
30-
const revokePuzzle = new Command.RevokePuzzleCommand()
30+
const revokePuzzle = new Command.RevokePuzzleCommand(
31+
puzzleStatusRepository,
32+
puzzleStatsRepository
33+
)
3134

3235
Object.assign(request, {
3336
createAnnouncementCommand,

0 commit comments

Comments
 (0)