Skip to content
This repository has been archived by the owner on Nov 14, 2024. It is now read-only.

Commit

Permalink
ajoute la fin de la machine
Browse files Browse the repository at this point in the history
  • Loading branch information
vmaubert authored and MichaelBitard committed Sep 16, 2024
1 parent d139c9b commit f58b937
Show file tree
Hide file tree
Showing 8 changed files with 333 additions and 131 deletions.
9 changes: 8 additions & 1 deletion packages/api/src/business/rules-demarches/machine-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { EtapeStatutId, EtapeStatutKey, ETAPES_STATUTS, isStatut, etapeStatutIdV
import { EtapeTypeId, etapeTypeIdValidator, isEtapeTypeId } from 'camino-common/src/static/etapesTypes'
import { ADMINISTRATION_IDS } from 'camino-common/src/static/administrations'
import { EtapeTypeEtapeStatut } from 'camino-common/src/static/etapesTypesEtapesStatuts'
import { DemarcheStatutId } from 'camino-common/src/static/demarchesStatuts'
import { DemarchesStatutsIds, DemarcheStatutId } from 'camino-common/src/static/demarchesStatuts'
import { CaminoDate, caminoDateValidator } from 'camino-common/src/date'
import { Departements, toDepartementId } from 'camino-common/src/static/departement'
import { Regions } from 'camino-common/src/static/region'
Expand All @@ -23,6 +23,13 @@ export interface Etape {
surface?: number
}

export const globalGuards = {
isVisibilitePublique: ({ context }) => context.visibilite === 'publique',
isVisibiliteConfidentielle: ({ context }) => context.visibilite === 'confidentielle',
isDemarcheStatutAcceptee: ({ context }) => context.demarcheStatut === DemarchesStatutsIds.Accepte,
isDemarcheStatutAccepteeEtPublie: ({ context }) => context.demarcheStatut === DemarchesStatutsIds.AccepteEtPublie,
} as const satisfies Record<string, (value: { context: CaminoCommonContext }) => boolean>

export interface CaminoCommonContext {
demarcheStatut: DemarcheStatutId
visibilite: 'confidentielle' | 'publique'
Expand Down
33 changes: 18 additions & 15 deletions packages/api/src/business/rules-demarches/machine-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { AnyMachineSnapshot, createActor, EventObject, MachineSnapshot, StateMac
import { CaminoCommonContext, DBEtat, Etape, Intervenant, intervenants, tags } from './machine-common'
import { DemarchesStatutsIds, DemarcheStatutId } from 'camino-common/src/static/demarchesStatuts'
import { CaminoDate } from 'camino-common/src/date'
import { OmitDistributive } from 'camino-common/src/typescript-tools'
import { isNotNullNorUndefined, OmitDistributive } from 'camino-common/src/typescript-tools'

type CaminoState<CaminoContext extends CaminoCommonContext, CaminoEvent extends EventObject> = MachineSnapshot<CaminoContext, CaminoEvent, any, any, any, any, any, any>

Expand Down Expand Up @@ -214,28 +214,31 @@ export abstract class CaminoMachine<CaminoContext extends CaminoCommonContext, C
return intervenants.filter(r => responsables.includes(tags.responsable[r]))
}

public possibleNextEvents(state: CaminoState<CaminoContext, CaminoEvent>, date: CaminoDate): CaminoEvent[] {
return getNextEvents(state)
.filter((event: string) => this.isEvent(event))
.flatMap(event => {
const events = this.toPotentialCaminoXStateEvent(event, date)

return events.filter(event => {
return state.can(event) && state.status !== 'done'
})
})
.filter(isNotNullNorUndefined)
.toSorted((a, b) => a.type.localeCompare(b.type))
}

public possibleNextEtapes(etapes: readonly Etape[], date: CaminoDate): (OmitDistributive<Etape, 'date' | 'titreTypeId' | 'demarcheTypeId'> & { mainStep: boolean })[] {
const state = this.assertGoTo(etapes)

if (state !== undefined) {
return getNextEvents(state)
.filter((event: string) => this.isEvent(event))
.flatMap(event => {
const events = this.toPotentialCaminoXStateEvent(event, date)

return events
.filter(event => {
return state.can(event) && state.status !== 'done'
})
.flatMap(event => this.caminoXStateEventToEtapes(event))
})
.filter(event => event !== undefined)
if (isNotNullNorUndefined(state)) {
return this.possibleNextEvents(state, date).flatMap(this.caminoXStateEventToEtapes).filter(isNotNullNorUndefined)
}

return []
}
}

export function getNextEvents(snapshot: AnyMachineSnapshot): string[] {
function getNextEvents(snapshot: AnyMachineSnapshot): string[] {
return [...new Set([...snapshot._nodes.flatMap(sn => sn.ownEvents)])]
}
78 changes: 58 additions & 20 deletions packages/api/src/business/rules-demarches/machine-test-helper.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { CaminoCommonContext, Etape } from './machine-common'
import { Actor, EventObject, createActor } from 'xstate'
import { CaminoMachine, getNextEvents } from './machine-helper'
import { CaminoMachine } from './machine-helper'
import { expect } from 'vitest'
import { CaminoDate, dateAddDays, toCaminoDate } from 'camino-common/src/date'
import { EtapeTypeEtapeStatutValidPair } from 'camino-common/src/static/etapesTypesEtapesStatuts'
import { isNotNullNorUndefined } from 'camino-common/src/typescript-tools'
import { isNotNullNorUndefined, onlyUnique } from 'camino-common/src/typescript-tools'
import { DemarchesStatuts } from 'camino-common/src/static/demarchesStatuts'
interface CustomMatchers<R = unknown> {
canOnlyTransitionTo<T extends EventObject, C extends CaminoCommonContext>(context: { machine: CaminoMachine<C, T>; date: CaminoDate }, _events: T['type'][]): R
}
Expand All @@ -19,21 +20,18 @@ declare global {
interface InverseAsymmetricMatchers extends CustomMatchers {}
}
}

expect.extend({
canOnlyTransitionTo<T extends EventObject, C extends CaminoCommonContext>(
service: Actor<CaminoMachine<C, T>['machine']>,
{ machine, date }: { machine: CaminoMachine<C, T>; date: CaminoDate },
events: T['type'][]
) {
events = events.toSorted()
const passEvents: (typeof events)[number][] = getNextEvents(service.getSnapshot())
.filter((event: string) => machine.isEvent(event))
.filter((event: (typeof events)[number]) => {
const events = machine.toPotentialCaminoXStateEvent(event, date)

return events.some(event => service.getSnapshot().can(event) && service.getSnapshot().status !== 'done')
})
.toSorted()
const passEvents = machine
.possibleNextEvents(service.getSnapshot(), date)
.map(({ type }) => type)
.filter(onlyUnique)

if (passEvents.length !== events.length || passEvents.some((entry, index) => entry !== events[index])) {
return {
Expand Down Expand Up @@ -62,14 +60,10 @@ export const interpretMachine = <T extends EventObject, C extends CaminoCommonCo
throw new Error(
`Error: cannot execute step: '${JSON.stringify(etapeAFaire)}' after '${JSON.stringify(
etapes.slice(0, i).map(etape => etape.etapeTypeId + '_' + etape.etapeStatutId)
)}'. The event ${JSON.stringify(event)} should be one of '${getNextEvents(service.getSnapshot())
.filter(event => machine.isEvent(event))
.filter((event: EventObject['type']) => {
const events = machine.toPotentialCaminoXStateEvent(event, etapeAFaire.date)
return events.some(event => service.getSnapshot().can(event) && service.getSnapshot().status !== 'done')
})
.toSorted()}'`
)}'. The event ${JSON.stringify(event)} should be one of '${machine
.possibleNextEvents(service.getSnapshot(), etapeAFaire.date)
.map(({ type }) => type)
.filter(onlyUnique)}'`
)
}
service.send(event)
Expand All @@ -78,11 +72,55 @@ export const interpretMachine = <T extends EventObject, C extends CaminoCommonCo
return service
}

export const getEventsTree = <T extends EventObject, C extends CaminoCommonContext>(
machine: CaminoMachine<C, T>,
initDate: `${number}-${number}-${number}`,
etapes: readonly (EtapeTypeEtapeStatutValidPair & Omit<Etape, 'date' | 'etapeTypeId' | 'etapeStatutId' | 'titreTypeId' | 'demarcheTypeId'> & { addDays?: number })[]
): string[] => {
const { service, dateFin } = setDateAndOrderAndInterpretMachine(machine, initDate, [])
const passEvents: T['type'][] = machine
.possibleNextEvents(service.getSnapshot(), dateFin)
.map(({ type }) => type)
.filter(onlyUnique)

const steps = [
{
type: 'RIEN',
visibilite: service.getSnapshot().context.visibilite,
demarcheStatut: DemarchesStatuts[service.getSnapshot().context.demarcheStatut].nom,
events: passEvents,
},
...etapes.map((_etape, index) => {
const etapesToLaunch = etapes.slice(0, index + 1)
const { service, dateFin, etapes: etapesWithDates } = setDateAndOrderAndInterpretMachine(machine, initDate, etapesToLaunch)

const passEvents: T['type'][] = machine
.possibleNextEvents(service.getSnapshot(), dateFin)
.map(({ type }) => type)
.filter(onlyUnique)
const event = machine.eventFrom(etapesWithDates[etapesWithDates.length - 1])

return {
type: event.type,
visibilite: service.getSnapshot().context.visibilite,
demarcheStatut: DemarchesStatuts[service.getSnapshot().context.demarcheStatut].nom,
events: passEvents,
}
}),
]

const maxPadType = Math.max(...steps.map(({ type }) => type.length))

return steps.map(step => {
return `${step.type.padEnd(maxPadType, ' ')} (${step.visibilite.padEnd(14, ' ')}, ${step.demarcheStatut.padEnd(23, ' ')}) -> [${step.events.join(',')}]`
})
}

export const setDateAndOrderAndInterpretMachine = <T extends EventObject, C extends CaminoCommonContext>(
machine: CaminoMachine<C, T>,
initDate: `${number}-${number}-${number}`,
etapes: readonly (EtapeTypeEtapeStatutValidPair & Omit<Etape, 'date' | 'etapeTypeId' | 'etapeStatutId' | 'titreTypeId' | 'demarcheTypeId'> & { addDays?: number })[]
): { service: Actor<(typeof machine)['machine']>; dateFin: CaminoDate; etapes: Etape[] } => {
): { service: Actor<(typeof machine)['machine']>; dateFin: CaminoDate; etapes: Etape[]; machine: CaminoMachine<C, T> } => {
const firstDate = toCaminoDate(initDate)
let index = 0
const fullEtapes = etapes.map(etape => {
Expand All @@ -96,7 +134,7 @@ export const setDateAndOrderAndInterpretMachine = <T extends EventObject, C exte
})
const service = orderAndInterpretMachine(machine, fullEtapes)

return { service, dateFin: dateAddDays(firstDate, etapes.length), etapes: fullEtapes }
return { service, dateFin: dateAddDays(firstDate, etapes.length), etapes: fullEtapes, machine }
}
export const orderAndInterpretMachine = <T extends EventObject, C extends CaminoCommonContext>(
machine: CaminoMachine<C, T>,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { assign, createMachine } from 'xstate'
import { assign, setup } from 'xstate'
import { CaminoMachine } from '../machine-helper'
import { CaminoCommonContext, DBEtat } from '../machine-common'
import { EtapesTypesEtapesStatuts as ETES } from 'camino-common/src/static/etapesTypesEtapesStatuts'
Expand Down Expand Up @@ -93,7 +93,7 @@ export class ProcedureSimplifieeMachine extends CaminoMachine<ProcedureSimplifie
{ type: event, status: ETAPES_STATUTS.TERMINE },
]
default:
return [{ type: event }]
return super.toPotentialCaminoXStateEvent(event, date)
}
}
}
Expand All @@ -105,8 +105,14 @@ interface ProcedureSimplifieeContext extends CaminoCommonContext {
const defaultDemarcheStatut = DemarchesStatutsIds.EnConstruction
const procedureHistoriqueDateMax = toCaminoDate('2024-07-01')
const procedureIncompleteDateMax = toCaminoDate('2000-01-01')
const procedureSimplifieeMachine = createMachine({
const procedureSimplifieeMachine = setup({
types: {} as { context: ProcedureSimplifieeContext; events: ProcedureSimplifieeXStateEvent },
guards: {
isProcedureHistorique: ({ context, event }) => 'date' in event && isBefore(event.date, procedureHistoriqueDateMax) && context.demarcheStatut === defaultDemarcheStatut,
isProcedureIncomplete: ({ context, event }) => 'date' in event && isBefore(event.date, procedureIncompleteDateMax) && context.demarcheStatut === defaultDemarcheStatut,
isDemarcheEnInstruction: ({ context }) => context.demarcheStatut === DemarchesStatutsIds.EnInstruction,
},
}).createMachine({
id: 'ProcedureSimplifiee',
initial: 'demandeAFaire',
context: {
Expand All @@ -117,73 +123,73 @@ const procedureSimplifieeMachine = createMachine({
},
on: {
RENDRE_DECISION_ADMINISTRATION_ACCEPTEE: {
guard: ({ context, event }) => isBefore(event.date, procedureHistoriqueDateMax) && context.demarcheStatut === defaultDemarcheStatut,
guard: 'isProcedureHistorique',
target: '.publicationAuRecueilDesActesAdministratifsOupublicationAuJORFAFaire',
actions: assign({
visibilite: 'publique',
demarcheStatut: DemarchesStatutsIds.Accepte,
}),
},
PUBLIER_DECISION_ACCEPTEE_AU_JORF: {
guard: ({ context, event }) => isBefore(event.date, procedureIncompleteDateMax) && context.demarcheStatut === defaultDemarcheStatut,
guard: 'isProcedureIncomplete',
target: '.abrogationAFaire',
actions: assign({
visibilite: 'publique',
demarcheStatut: DemarchesStatutsIds.AccepteEtPublie,
}),
},
PUBLIER_DECISION_AU_RECUEIL_DES_ACTES_ADMINISTRATIFS: {
guard: ({ context, event }) => isBefore(event.date, procedureIncompleteDateMax) && context.demarcheStatut === defaultDemarcheStatut,
guard: 'isProcedureIncomplete',
target: '.abrogationAFaire',
actions: assign({
visibilite: 'publique',
demarcheStatut: DemarchesStatutsIds.AccepteEtPublie,
}),
},
SAISIR_INFORMATION_HISTORIQUE_INCOMPLETE: {
guard: ({ context, event }) => isBefore(event.date, procedureIncompleteDateMax) && context.demarcheStatut === defaultDemarcheStatut,
guard: 'isProcedureIncomplete',
target: '.finDeMachine',
actions: assign({
visibilite: 'confidentielle',
demarcheStatut: DemarchesStatutsIds.Accepte,
}),
},
RENDRE_DECISION_ADMINISTRATION_REJETEE: {
guard: ({ context, event }) => isBefore(event.date, procedureHistoriqueDateMax) && context.demarcheStatut === defaultDemarcheStatut,
guard: 'isProcedureHistorique',
target: '.publicationAuJorfApresRejetAFaire',
actions: assign({
visibilite: 'confidentielle',
demarcheStatut: DemarchesStatutsIds.Rejete,
}),
},
RENDRE_DECISION_ADMINISTRATION_REJETEE_DECISION_IMPLICITE: {
guard: ({ context, event }) => isBefore(event.date, procedureHistoriqueDateMax) && context.demarcheStatut === defaultDemarcheStatut,
guard: 'isProcedureHistorique',
target: '.finDeMachine',
actions: assign({
visibilite: 'publique',
demarcheStatut: DemarchesStatutsIds.Rejete,
}),
},
CLASSER_SANS_SUITE: {
guard: ({ context }) => context.demarcheStatut === DemarchesStatutsIds.EnInstruction,
guard: 'isDemarcheEnInstruction',
target: '.finDeMachine',
actions: assign({
demarcheStatut: DemarchesStatutsIds.ClasseSansSuite,
}),
},
DESISTER_PAR_LE_DEMANDEUR: {
guard: ({ context }) => context.demarcheStatut === DemarchesStatutsIds.EnInstruction,
guard: 'isDemarcheEnInstruction',
target: '.finDeMachine',
actions: assign({
demarcheStatut: DemarchesStatutsIds.Desiste,
}),
},
DEMANDER_INFORMATION: {
guard: ({ context }) => context.demarcheStatut === DemarchesStatutsIds.EnInstruction,
guard: 'isDemarcheEnInstruction',
actions: assign({}),
},
RECEVOIR_INFORMATION: {
guard: ({ context }) => context.demarcheStatut === DemarchesStatutsIds.EnInstruction,
guard: 'isDemarcheEnInstruction',
actions: assign({}),
},
},
Expand Down
Loading

0 comments on commit f58b937

Please sign in to comment.