From 61be3e9a66d026cabdadbaf05658a43e932f6d7d Mon Sep 17 00:00:00 2001 From: Oskar Dudycz Date: Wed, 23 Oct 2024 10:37:30 +0200 Subject: [PATCH] Adjusted sample to showcase idempotence handling It shows idempotence both upon the new stream creation (check in) and existing stream (check out). --- src/guestStayAccounts/api/api.e2e.spec.ts | 8 +++---- src/guestStayAccounts/api/api.int.spec.ts | 23 ++++--------------- src/guestStayAccounts/api/api.ts | 13 ++++++----- src/guestStayAccounts/businessLogic.ts | 23 ++++++++----------- .../businessLogic.unit.spec.ts | 20 ++++------------ 5 files changed, 28 insertions(+), 59 deletions(-) diff --git a/src/guestStayAccounts/api/api.e2e.spec.ts b/src/guestStayAccounts/api/api.e2e.spec.ts index 0555771..e851d63 100644 --- a/src/guestStayAccounts/api/api.e2e.spec.ts +++ b/src/guestStayAccounts/api/api.e2e.spec.ts @@ -147,10 +147,10 @@ void describe('guestStayAccount E2E', () => { void describe('When checked in', () => { const checkedInAccount: TestRequest = checkIn; - void it(`doesn't check in`, () => + void it(`ignores check in`, () => given(checkedInAccount) .when(checkIn) - .then([expectError(403, { detail: `Guest is already checked-in!` })])); + .then([expectResponse(201)])); void it('records charge', () => given(checkedInAccount) @@ -299,10 +299,10 @@ void describe('guestStayAccount E2E', () => { expectError(403, { detail: `Guest account is already checked out` }), ])); - void it(`doesn't checkout`, () => + void it(`ignores checkout`, () => given(...checkedOutAccount) .when(checkOut) - .then([expectError(403, { detail: `NotCheckedIn` })])); + .then([expectResponse(204)])); void it(`details return 404`, () => given(...checkedOutAccount) diff --git a/src/guestStayAccounts/api/api.int.spec.ts b/src/guestStayAccounts/api/api.int.spec.ts index e822b97..5d0c724 100644 --- a/src/guestStayAccounts/api/api.int.spec.ts +++ b/src/guestStayAccounts/api/api.int.spec.ts @@ -126,10 +126,8 @@ void describe('Guest stay account', () => { ])), ); - void it(`doesn't check in`, () => - given(checkedInAccount) - .when(checkIn) - .then(expectError(403, { detail: `Guest is already checked-in!` }))); + void it(`ingores check in`, () => + given(checkedInAccount).when(checkIn).then(expectResponse(201))); void it('records charge', () => given(checkedInAccount) @@ -422,23 +420,10 @@ void describe('Guest stay account', () => { expectError(403, { detail: `Guest account is already checked out` }), )); - void it(`doesn't checkout`, () => + void it(`ignores check out`, () => given(checkedOutAccount) .when(checkOut) - .then([ - expectError(403, { detail: `NotCheckedIn` }), - expectNewEvents(guestStayAccountId, [ - { - type: 'GuestCheckoutFailed', - data: { - guestStayAccountId, - groupCheckoutId: undefined, - reason: 'NotCheckedIn', - failedAt: now, - }, - }, - ]), - ])); + .then([expectResponse(204)])); }); const given = ApiSpecification.for( diff --git a/src/guestStayAccounts/api/api.ts b/src/guestStayAccounts/api/api.ts index 927aedc..2231f31 100644 --- a/src/guestStayAccounts/api/api.ts +++ b/src/guestStayAccounts/api/api.ts @@ -168,16 +168,17 @@ export const guestStayAccountsApi = metadata: { now: getCurrentTime() }, }; - const { - newEvents: [event], - } = await handle(eventStore, guestStayAccountId, (state) => - checkOut(command, state), + const { newEvents } = await handle( + eventStore, + guestStayAccountId, + (state) => checkOut(command, state), ); - return event.type !== 'GuestCheckoutFailed' + return newEvents.length === 0 || + newEvents[0].type !== 'GuestCheckoutFailed' ? NoContent() : Forbidden({ - problemDetails: event.data.reason, + problemDetails: newEvents[0].data.reason, }); }), ); diff --git a/src/guestStayAccounts/businessLogic.ts b/src/guestStayAccounts/businessLogic.ts index 07073ad..a7a3bac 100644 --- a/src/guestStayAccounts/businessLogic.ts +++ b/src/guestStayAccounts/businessLogic.ts @@ -54,8 +54,11 @@ export type GuestStayCommand = export const checkIn = ( { data: { guestId, roomId }, metadata }: CheckIn, state: GuestStayAccount, -): GuestCheckedIn => { - assertDoesNotExist(state); +): GuestCheckedIn | [] => { + if (state.status === 'CheckedIn') []; + + if (state.status === 'CheckedOut') + throw new IllegalStateError(`Guest account is already checked out`); const now = metadata?.now ?? new Date(); @@ -107,7 +110,9 @@ export const recordPayment = ( export const checkOut = ( { data: { guestStayAccountId, groupCheckoutId }, metadata }: CheckOut, state: GuestStayAccount, -): GuestCheckedOut | GuestCheckoutFailed => { +): GuestCheckedOut | GuestCheckoutFailed | [] => { + if (state.status === 'CheckedOut') return []; + const now = metadata?.now ?? new Date(); if (state.status !== 'CheckedIn') @@ -146,7 +151,7 @@ export const checkOut = ( export const decide = ( command: GuestStayCommand, state: GuestStayAccount, -): GuestStayAccountEvent => { +): GuestStayAccountEvent | GuestStayAccountEvent[] => { const { type } = command; switch (type) { @@ -165,16 +170,6 @@ export const decide = ( } }; -const assertDoesNotExist = (state: GuestStayAccount): state is CheckedIn => { - if (state.status === 'CheckedIn') - throw new IllegalStateError(`Guest is already checked-in!`); - - if (state.status === 'CheckedOut') - throw new IllegalStateError(`Guest account is already checked out`); - - return true; -}; - const assertIsCheckedIn = (state: GuestStayAccount): state is CheckedIn => { if (state.status === 'NotExisting') throw new IllegalStateError(`Guest account doesn't exist!`); diff --git a/src/guestStayAccounts/businessLogic.unit.spec.ts b/src/guestStayAccounts/businessLogic.unit.spec.ts index d445e92..ebaf2a3 100644 --- a/src/guestStayAccounts/businessLogic.unit.spec.ts +++ b/src/guestStayAccounts/businessLogic.unit.spec.ts @@ -121,7 +121,7 @@ void describe('Guest Stay Account', () => { }, ]; - void it(`doesn't check in`, () => + void it(`ignores check in`, () => given(checkedInAccount) .when({ type: 'CheckIn', @@ -131,9 +131,7 @@ void describe('Guest Stay Account', () => { }, metadata: { now }, }) - .thenThrows( - (error) => error.message === `Guest is already checked-in!`, - )); + .then([])); void it('records charge', () => { given(checkedInAccount) @@ -476,7 +474,7 @@ void describe('Guest Stay Account', () => { (error) => error.message === `Guest account is already checked out`, )); - void it(`doesn't checkout`, () => + void it(`ignores check out`, () => given(checkedOutAccount) .when({ type: 'CheckOut', @@ -485,16 +483,6 @@ void describe('Guest Stay Account', () => { }, metadata: { now }, }) - .then([ - { - type: 'GuestCheckoutFailed', - data: { - guestStayAccountId, - groupCheckoutId: undefined, - reason: 'NotCheckedIn', - failedAt: now, - }, - }, - ])); + .then([])); }); });