diff --git a/samples/hotelManagement/.vscode/settings.json b/samples/hotelManagement/.vscode/settings.json index 8f94129f..2d91860e 100644 --- a/samples/hotelManagement/.vscode/settings.json +++ b/samples/hotelManagement/.vscode/settings.json @@ -4,7 +4,9 @@ "editor.formatOnSave": true, "editor.codeActionsOnSave": { - "source.fixAll.eslint": "explicit" + "source.organizeImports": "explicit", + "source.fixAll.eslint": "explicit", + "source.addMissingImports": "always" }, "editor.tabSize": 2, @@ -13,5 +15,7 @@ "node_modules/": true, "dist/": true }, - "files.eol": "\n" + "files.eol": "\n", + + "typescript.preferences.importModuleSpecifier": "relative" } diff --git a/samples/hotelManagement/package-lock.json b/samples/hotelManagement/package-lock.json index acc7ff2f..526f9a43 100644 --- a/samples/hotelManagement/package-lock.json +++ b/samples/hotelManagement/package-lock.json @@ -16,7 +16,8 @@ "dotenv-cli": "7.3.0", "express": "4.18.2", "immutable": "5.0.0-beta.4", - "mongodb": "6.3.0" + "mongodb": "6.3.0", + "ts-essentials": "9.4.1" }, "devDependencies": { "@types/convict": "6.1.6", @@ -8661,6 +8662,19 @@ "typescript": ">=4.2.0" } }, + "node_modules/ts-essentials": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-9.4.1.tgz", + "integrity": "sha512-oke0rI2EN9pzHsesdmrOrnqv1eQODmJpd/noJjwj2ZPC3Z4N2wbjrOEqnsEgmvlO2+4fBb0a794DCna2elEVIQ==", + "peerDependencies": { + "typescript": ">=4.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/ts-jest": { "version": "29.1.1", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz", @@ -8831,7 +8845,7 @@ "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15880,6 +15894,12 @@ "dev": true, "requires": {} }, + "ts-essentials": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-9.4.1.tgz", + "integrity": "sha512-oke0rI2EN9pzHsesdmrOrnqv1eQODmJpd/noJjwj2ZPC3Z4N2wbjrOEqnsEgmvlO2+4fBb0a794DCna2elEVIQ==", + "requires": {} + }, "ts-jest": { "version": "29.1.1", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz", @@ -15982,7 +16002,7 @@ "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", - "dev": true + "devOptional": true }, "unbox-primitive": { "version": "1.0.1", diff --git a/samples/hotelManagement/package.json b/samples/hotelManagement/package.json index 409827b0..c19d1dfe 100644 --- a/samples/hotelManagement/package.json +++ b/samples/hotelManagement/package.json @@ -41,7 +41,8 @@ "dotenv-cli": "7.3.0", "express": "4.18.2", "immutable": "5.0.0-beta.4", - "mongodb": "6.3.0" + "mongodb": "6.3.0", + "ts-essentials": "9.4.1" }, "devDependencies": { "@types/convict": "6.1.6", diff --git a/samples/hotelManagement/src/core/command.ts b/samples/hotelManagement/src/core/command.ts index 9225c029..f598ed62 100644 --- a/samples/hotelManagement/src/core/command.ts +++ b/samples/hotelManagement/src/core/command.ts @@ -1,7 +1,12 @@ +import { Flavour } from './typing'; + export type Command< CommandType extends string = string, CommandData extends Record = Record, -> = Readonly<{ - type: Readonly; - data: Readonly; -}>; +> = Flavour< + Readonly<{ + type: Readonly; + data: Readonly; + }>, + 'Command' +>; diff --git a/samples/hotelManagement/src/core/event.ts b/samples/hotelManagement/src/core/event.ts index 343d37e6..8c2d6662 100644 --- a/samples/hotelManagement/src/core/event.ts +++ b/samples/hotelManagement/src/core/event.ts @@ -1,7 +1,12 @@ +import { Flavour } from './typing'; + export type Event< EventType extends string = string, EventData extends Record = Record, -> = Readonly<{ - type: Readonly; - data: Readonly; -}>; +> = Flavour< + Readonly<{ + type: Readonly; + data: Readonly; + }>, + 'Event' +>; diff --git a/samples/hotelManagement/src/core/processManager.ts b/samples/hotelManagement/src/core/processManager.ts deleted file mode 100644 index 0dd33842..00000000 --- a/samples/hotelManagement/src/core/processManager.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Event } from './event'; -import { Command } from './command'; - -export type EnqueuedEvent = Readonly<{ - type: 'Event'; - data: EventType; -}>; - -export type ScheduledCommand = Readonly<{ - type: 'Command'; - data: CommandType; -}>; - -export type ProcesssingResult< - CommandType extends Command, - EventType extends Event, -> = ScheduledCommand | EnqueuedEvent; - -export const enqueue = ( - event: EventType, -): EnqueuedEvent => { - return { type: 'Event', data: event }; -}; - -export const schedule = ( - command: CommandType, -): ScheduledCommand => { - return { type: 'Command', data: command }; -}; diff --git a/samples/hotelManagement/src/core/typing.ts b/samples/hotelManagement/src/core/typing.ts new file mode 100644 index 00000000..0b5cb6fb --- /dev/null +++ b/samples/hotelManagement/src/core/typing.ts @@ -0,0 +1,2 @@ +export type Brand = K & { readonly __brand: T }; +export type Flavour = K & { readonly __brand?: T }; diff --git a/samples/hotelManagement/src/core/workflow.ts b/samples/hotelManagement/src/core/workflow.ts new file mode 100644 index 00000000..96747378 --- /dev/null +++ b/samples/hotelManagement/src/core/workflow.ts @@ -0,0 +1,19 @@ +import { Command } from './command'; +import { Event } from './event'; + +/// Inspired by https://blog.bittacklr.be/the-workflow-pattern.html + +export type WorkflowEvent = Extract< + Output, + { __brand?: 'Event' } +>; + +export type Workflow< + Input extends Event | Command, + State, + Output extends Event | Command, +> = { + decide: (command: Input, state: State) => Output[]; + evolve: (currentState: State, event: WorkflowEvent) => State; + getInitialState: () => State; +}; diff --git a/samples/hotelManagement/src/processManagers/groupCheckouts/groupCheckoutProcessManager.ts b/samples/hotelManagement/src/processManagers/groupCheckouts/groupCheckoutProcessManager.ts index 4d7ecda0..5c511bb7 100644 --- a/samples/hotelManagement/src/processManagers/groupCheckouts/groupCheckoutProcessManager.ts +++ b/samples/hotelManagement/src/processManagers/groupCheckouts/groupCheckoutProcessManager.ts @@ -1,6 +1,5 @@ -import { Map } from 'immutable'; import { Command } from '#core/command'; -import { enqueue, ProcesssingResult, schedule } from '#core/processManager'; +import { Map } from 'immutable'; import { CheckOut, GuestCheckedOut, @@ -19,10 +18,7 @@ export type GroupCheckoutProcessManagerEvent = | GuestCheckedOut | GuestCheckoutFailed; -export type GroupCheckoutProcessingResult = ProcesssingResult< - CheckOut, - GroupCheckoutEvent ->; +export type GroupCheckoutProcessingResult = CheckOut | GroupCheckoutEvent; export type InitiateGroupCheckout = Command< 'InitiateGroupCheckout', @@ -68,27 +64,29 @@ export const GroupCheckoutProcessManager = ( if (state.status === 'Finished') return []; - const checkoutGuestStays = event.guestStayAccountIds.map((id) => { - return schedule({ - type: 'CheckOut', - data: { - guestStayAccountId: id, - now: event.initiatedAt, - groupCheckoutId: event.groupCheckoutId, - }, - }); - }); + const checkoutGuestStays: CheckOut[] = event.guestStayAccountIds.map( + (id) => { + return { + type: 'CheckOut', + data: { + guestStayAccountId: id, + now: event.initiatedAt, + groupCheckoutId: event.groupCheckoutId, + }, + }; + }, + ); return [ ...checkoutGuestStays, - enqueue({ + { type: 'GuestCheckoutsInitiated', data: { groupCheckoutId: event.groupCheckoutId, initiatedGuestStayIds: event.guestStayAccountIds, initiatedAt: event.initiatedAt, }, - }), + }, ]; } @@ -137,11 +135,8 @@ export const GroupCheckoutProcessManager = ( }; return areAnyOngoingCheckouts(guestStayAccountIds) - ? enqueue(finished) - : [ - enqueue(finished), - enqueue(finish(groupCheckoutId, state.guestStayAccountIds, now)), - ]; + ? finished + : [finished, finish(groupCheckoutId, state.guestStayAccountIds, now)]; } } }; diff --git a/samples/hotelManagement/src/workflows/groupCheckouts/groupCheckoutWorflow.ts b/samples/hotelManagement/src/workflows/groupCheckouts/groupCheckoutWorflow.ts new file mode 100644 index 00000000..99743d1a --- /dev/null +++ b/samples/hotelManagement/src/workflows/groupCheckouts/groupCheckoutWorflow.ts @@ -0,0 +1,386 @@ +import { Command } from '#core/command'; +import { Event } from '#core/event'; +import { Workflow, WorkflowEvent } from '#core/workflow'; +import { Map } from 'immutable'; +import { + CheckOut, + GuestCheckedOut, + GuestCheckoutFailed, +} from '../guestStayAccounts/guestStayAccount'; + +/// Inspired by https://blog.bittacklr.be/the-workflow-pattern.html + +//////////////////////////////////////////// +////////// Commands +/////////////////////////////////////////// + +export type InitiateGroupCheckout = Command< + 'InitiateGroupCheckout', + { + groupCheckoutId: string; + clerkId: string; + guestStayAccountIds: string[]; + now: Date; + } +>; + +//////////////////////////////////////////// +////////// EVENTS +/////////////////////////////////////////// + +export type GroupCheckoutInitiated = Event< + 'GroupCheckoutInitiated', + { + groupCheckoutId: string; + clerkId: string; + guestStayAccountIds: string[]; + initiatedAt: Date; + } +>; + +export type GuestCheckoutsInitiated = Event< + 'GuestCheckoutsInitiated', + { + groupCheckoutId: string; + initiatedGuestStayIds: string[]; + initiatedAt: Date; + } +>; + +export type GuestCheckoutCompletionRecorded = Event< + 'GuestCheckoutCompletionRecorded', + { + groupCheckoutId: string; + guestStayAccountId: string; + completedAt: Date; + } +>; + +export type GuestCheckoutFailureRecorded = Event< + 'GuestCheckoutFailureRecorded', + { + groupCheckoutId: string; + guestStayAccountId: string; + failedAt: Date; + } +>; + +export type GroupCheckoutCompleted = Event< + 'GroupCheckoutCompleted', + { + groupCheckoutId: string; + completedCheckouts: string[]; + completedAt: Date; + } +>; + +export type GroupCheckoutFailed = Event< + 'GroupCheckoutFailed', + { + groupCheckoutId: string; + completedCheckouts: string[]; + failedCheckouts: string[]; + failedAt: Date; + } +>; + +export type GroupCheckoutEvent = + | GroupCheckoutInitiated + | GuestCheckoutsInitiated + | GuestCheckoutCompletionRecorded + | GuestCheckoutFailureRecorded + | GroupCheckoutCompleted + | GroupCheckoutFailed; + +//////////////////////////////////////////// +////////// Entity +/////////////////////////////////////////// + +export type GroupCheckout = + | { status: 'NotExisting' } + | { + status: 'Pending'; + guestStayAccountIds: Map; + } + | { status: 'Finished' }; + +export const getInitialState = (): GroupCheckout => { + return { + status: 'NotExisting', + }; +}; + +export enum GuestStayStatus { + Pending = 'Pending', + Initiated = 'Initiated', + Completed = 'Completed', + Failed = 'Failed', +} + +//////////////////////////////////////////// +////////// Workflow Definition +/////////////////////////////////////////// + +export type GroupCheckoutInput = + | InitiateGroupCheckout + | GuestCheckedOut + | GuestCheckoutFailed; + +export type GroupCheckoutOutput = + | GroupCheckoutInitiated + | CheckOut + | GuestCheckoutCompletionRecorded + | GuestCheckoutFailureRecorded + | GuestCheckoutsInitiated + | GroupCheckoutCompleted + | GroupCheckoutFailed + | Ignored + | UnexpectedErrorOcurred; +export type Ignored = { + type: 'Ignored'; + data: { + reason: IgnoredReason; + }; +}; + +export type UnexpectedErrorOcurred = { + type: 'ErrorOcurred'; + data: { + reason: ErrorReason; + }; +}; + +export type IgnoredReason = + | 'GroupCheckoutAlreadyInitiated' + | 'GuestCheckoutsInitiationAlreadyRecorded' + | 'GuestCheckoutAlreadyFinished' + | 'GroupCheckoutAlreadyFinished' + | 'GroupCheckoutDoesNotExist'; + +export type ErrorReason = 'UnknownInputType'; + +const ignore = (reason: IgnoredReason): Ignored => { + return { + type: 'Ignored', + data: { + reason, + }, + }; +}; + +const error = (reason: ErrorReason): UnexpectedErrorOcurred => { + return { + type: 'ErrorOcurred', + data: { + reason, + }, + }; +}; + +//////////////////////////////////////////// +////////// Evolve +/////////////////////////////////////////// + +export const decide = ( + input: GroupCheckoutInput, + state: GroupCheckout, +): GroupCheckoutOutput[] => { + const { type, data } = input; + + switch (type) { + case 'InitiateGroupCheckout': { + if (state.status !== 'NotExisting') + return [ignore('GroupCheckoutAlreadyInitiated')]; + + const checkoutGuestStays = data.guestStayAccountIds.map( + (id) => { + return { + type: 'CheckOut', + data: { + guestStayAccountId: id, + now: data.now, + groupCheckoutId: data.groupCheckoutId, + }, + }; + }, + ); + + return [ + { + type: 'GuestCheckoutsInitiated', + data: { + groupCheckoutId: data.groupCheckoutId, + initiatedGuestStayIds: data.guestStayAccountIds, + initiatedAt: data.now, + }, + }, + ...checkoutGuestStays, + ]; + } + case 'GuestCheckedOut': + case 'GuestCheckoutFailed': { + if (!data.groupCheckoutId) return []; + + if (state.status === 'NotExisting') return []; + + if (state.status === 'Finished') return []; + + const { guestStayAccountId, groupCheckoutId } = data; + + const guestCheckoutStatus = + state.guestStayAccountIds.get(guestStayAccountId); + + if (isAlreadyClosed(guestCheckoutStatus)) return []; + + const guestStayAccountIds = state.guestStayAccountIds.set( + guestStayAccountId, + type === 'GuestCheckedOut' + ? GuestStayStatus.Completed + : GuestStayStatus.Failed, + ); + + const now = + type === 'GuestCheckedOut' ? data.checkedOutAt : data.failedAt; + + const finished: GroupCheckoutEvent = + type === 'GuestCheckedOut' + ? { + type: 'GuestCheckoutCompletionRecorded', + data: { + groupCheckoutId, + guestStayAccountId, + completedAt: now, + }, + } + : { + type: 'GuestCheckoutFailureRecorded', + data: { + groupCheckoutId, + guestStayAccountId, + failedAt: now, + }, + }; + + return areAnyOngoingCheckouts(guestStayAccountIds) + ? [finished] + : [finished, finish(groupCheckoutId, state.guestStayAccountIds, now)]; + } + default: { + const _notExistingEventType: never = type; + return [error('UnknownInputType')]; + } + } +}; + +export const evolve = ( + state: GroupCheckout, + { type, data: event }: WorkflowEvent, +): GroupCheckout => { + switch (type) { + case 'GroupCheckoutInitiated': { + if (state.status !== 'NotExisting') return state; + + return { + status: 'Pending', + guestStayAccountIds: event.guestStayAccountIds.reduce( + (map, id) => map.set(id, GuestStayStatus.Pending), + Map(), + ), + }; + } + case 'GuestCheckoutsInitiated': { + if (state.status !== 'Pending') return state; + + return { + status: 'Pending', + guestStayAccountIds: event.initiatedGuestStayIds.reduce( + (map, id) => map.set(id, GuestStayStatus.Initiated), + state.guestStayAccountIds, + ), + }; + } + case 'GuestCheckoutCompletionRecorded': + case 'GuestCheckoutFailureRecorded': { + if (state.status !== 'Pending') return state; + + return { + ...state, + guestStayAccountIds: state.guestStayAccountIds.set( + event.guestStayAccountId, + type === 'GuestCheckoutCompletionRecorded' + ? GuestStayStatus.Completed + : GuestStayStatus.Failed, + ), + }; + } + case 'GroupCheckoutCompleted': + case 'GroupCheckoutFailed': { + if (state.status !== 'Pending') return state; + + return { + status: 'Finished', + }; + } + default: { + const _notExistingEventType: never = type; + return state; + } + } +}; + +export const GroupCheckoutWorkflow: Workflow< + GroupCheckoutInput, + GroupCheckout, + GroupCheckoutOutput +> = { + decide, + evolve, + getInitialState, +}; + +export const isAlreadyClosed = (status: GuestStayStatus | undefined) => + status === GuestStayStatus.Completed || status === GuestStayStatus.Failed; + +const areAnyOngoingCheckouts = ( + guestStayAccounts: Map, +) => guestStayAccounts.some((status) => !isAlreadyClosed(status)); + +const areAllCompleted = (guestStayAccounts: Map) => + guestStayAccounts.some((status) => status === GuestStayStatus.Completed); + +const checkoutsWith = ( + guestStayAccounts: Map, + status: GuestStayStatus, +): string[] => + Array.from(guestStayAccounts.filter((s) => s === status).values()); + +const finish = ( + groupCheckoutId: string, + guestStayAccounts: Map, + now: Date, +): GroupCheckoutEvent => { + return areAllCompleted(guestStayAccounts) + ? { + type: 'GroupCheckoutCompleted', + data: { + groupCheckoutId, + completedCheckouts: Array.from(guestStayAccounts.values()), + completedAt: now, + }, + } + : { + type: 'GroupCheckoutFailed', + data: { + groupCheckoutId, + completedCheckouts: checkoutsWith( + guestStayAccounts, + GuestStayStatus.Completed, + ), + failedCheckouts: checkoutsWith( + guestStayAccounts, + GuestStayStatus.Failed, + ), + failedAt: now, + }, + }; +}; diff --git a/samples/hotelManagement/src/workflows/guestStayAccounts/guestStayAccount.ts b/samples/hotelManagement/src/workflows/guestStayAccounts/guestStayAccount.ts new file mode 100644 index 00000000..6f323aad --- /dev/null +++ b/samples/hotelManagement/src/workflows/guestStayAccounts/guestStayAccount.ts @@ -0,0 +1,245 @@ +import { Command } from '#core/command'; +import { Event } from '#core/event'; + +//////////////////////////////////////////// +////////// EVENTS +/////////////////////////////////////////// + +export type GuestCheckedIn = Event< + 'GuestCheckedIn', + { + guestStayAccountId: string; + checkedInAt: Date; + } +>; + +export type ChargeRecorded = Event< + 'ChargeRecorded', + { + guestStayAccountId: string; + amount: number; + recordedAt: Date; + } +>; + +export type PaymentRecorded = Event< + 'PaymentRecorded', + { + guestStayAccountId: string; + amount: number; + recordedAt: Date; + } +>; +export type GuestCheckedOut = Event< + 'GuestCheckedOut', + { + guestStayAccountId: string; + checkedOutAt: Date; + groupCheckoutId?: string; + } +>; + +export type GuestCheckoutFailed = Event< + 'GuestCheckoutFailed', + { + guestStayAccountId: string; + reason: 'NotOpened' | 'BalanceNotSettled'; + failedAt: Date; + groupCheckoutId?: string; + } +>; + +export type GuestStayAccountEvent = + | GuestCheckedIn + | ChargeRecorded + | PaymentRecorded + | GuestCheckedOut + | GuestCheckoutFailed; + +//////////////////////////////////////////// +////////// Entity +/////////////////////////////////////////// + +export type GuestStayAccount = + | { status: 'NotExisting' } + | { + status: 'Opened'; + balance: number; + } + | { status: 'CheckedOut' }; + +//////////////////////////////////////////// +////////// Evolve +/////////////////////////////////////////// + +export const evolve = ( + state: GuestStayAccount, + { type, data: event }: GuestStayAccountEvent, +): GuestStayAccount => { + switch (type) { + case 'GuestCheckedIn': { + if (state.status !== 'NotExisting') return state; + + return { status: 'Opened', balance: 0 }; + } + case 'ChargeRecorded': { + if (state.status !== 'Opened') return state; + + return { + ...state, + balance: state.balance - event.amount, + }; + } + case 'PaymentRecorded': { + if (state.status !== 'Opened') return state; + + return { + ...state, + balance: state.balance + event.amount, + }; + } + case 'GuestCheckedOut': { + if (state.status !== 'Opened') return state; + + return { + status: 'CheckedOut', + }; + } + case 'GuestCheckoutFailed': { + return state; + } + default: { + const _notExistingEventType: never = type; + return state; + } + } +}; + +//////////////////////////////////////////// +////////// Commands +/////////////////////////////////////////// + +export type CheckIn = Command< + 'CheckIn', + { + guestStayAccountId: string; + now: Date; + } +>; +export type RecordCharge = Command< + 'RecordCharge', + { + guestStayAccountId: string; + amount: number; + now: Date; + } +>; +export type RecordPayment = Command< + 'RecordPayment', + { + guestStayAccountId: string; + amount: number; + now: Date; + } +>; +export type CheckOut = Command< + 'CheckOut', + { + guestStayAccountId: string; + now: Date; + groupCheckoutId?: string; + } +>; + +export type GuestStayCommand = + | CheckIn + | RecordCharge + | RecordPayment + | CheckOut; + +export const decide = ( + { type, data: command }: GuestStayCommand, + state: GuestStayAccount, +): GuestStayAccountEvent => { + const { guestStayAccountId, now } = command; + + switch (type) { + case 'CheckIn': { + if (state.status !== 'NotExisting') + throw Error('Guest is already checked-in!'); + + return { + type: 'GuestCheckedIn', + data: { + guestStayAccountId, + checkedInAt: now, + }, + }; + } + case 'RecordCharge': { + if (state.status !== 'Opened') + throw Error('Guest account is already checked out!'); + + return { + type: 'ChargeRecorded', + data: { + guestStayAccountId, + amount: command.amount, + recordedAt: now, + }, + }; + } + case 'RecordPayment': { + if (state.status !== 'Opened') + throw Error('Guest account is already checked out!'); + + return { + type: 'PaymentRecorded', + data: { + guestStayAccountId, + amount: command.amount, + recordedAt: now, + }, + }; + } + case 'CheckOut': { + if (state.status !== 'Opened') + return { + type: 'GuestCheckoutFailed', + data: { + guestStayAccountId, + groupCheckoutId: command.groupCheckoutId, + reason: 'NotOpened', + failedAt: now, + }, + }; + + const isSettled = state.balance === 0; + + if (!isSettled) + return { + type: 'GuestCheckoutFailed', + data: { + guestStayAccountId, + groupCheckoutId: command.groupCheckoutId, + reason: 'BalanceNotSettled', + failedAt: now, + }, + }; + + return { + type: 'GuestCheckedOut', + data: { + guestStayAccountId, + groupCheckoutId: command.groupCheckoutId, + checkedOutAt: now, + }, + }; + } + + default: { + const _notExistingCommandType: never = type; + throw new Error(`Unknown command type`); + } + } +}; diff --git a/samples/hotelManagement/src/workflows/guestStayAccounts/guestStayAccountDomainService.ts b/samples/hotelManagement/src/workflows/guestStayAccounts/guestStayAccountDomainService.ts new file mode 100644 index 00000000..1abc9fd5 --- /dev/null +++ b/samples/hotelManagement/src/workflows/guestStayAccounts/guestStayAccountDomainService.ts @@ -0,0 +1 @@ +export const nothing = {}; diff --git a/workshops/introduction_to_event_sourcing/.vscode/settings.json b/workshops/introduction_to_event_sourcing/.vscode/settings.json index 8c5e9a4f..2d91860e 100644 --- a/workshops/introduction_to_event_sourcing/.vscode/settings.json +++ b/workshops/introduction_to_event_sourcing/.vscode/settings.json @@ -4,7 +4,7 @@ "editor.formatOnSave": true, "editor.codeActionsOnSave": { - "source.organizeImports": true, + "source.organizeImports": "explicit", "source.fixAll.eslint": "explicit", "source.addMissingImports": "always" },