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

feat(machine): améliore la machine spécifique #1465

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions packages/api/src/business/rules-demarches/arm/oct.machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,17 +285,17 @@ interface OctARMContext extends CaminoCommonContext {
paiementFraisDossierValide: boolean
}

const isConnu = (mecanisation: MecanisationInconnu): mecanisation is MecanisationConnu => {
const isConnu = (mecanisation: MecanisationInconnu) => {
return mecanisation !== 'inconnu'
}
const isInconnu = (mecanisation: MecanisationInconnu): mecanisation is MecanisationInconnu => {
const isInconnu = (mecanisation: MecanisationInconnu) => {
return !isConnu(mecanisation)
}

const isMecanise = (mecanisation: MecanisationInconnu): mecanisation is MecanisationConnuMecanise => {
const isMecanise = (mecanisation: MecanisationInconnu) => {
return isConnu(mecanisation) && mecanisation.mecanise
}
const isNonMecanise = (mecanisation: MecanisationInconnu): mecanisation is MecanisationConnuNonMecanise => {
const isNonMecanise = (mecanisation: MecanisationInconnu) => {
return isConnu(mecanisation) && !mecanisation.mecanise
}
const mustPayerFraisDossierComplementaire = (mecanisation: MecanisationInconnu): boolean => {
Expand Down
2 changes: 1 addition & 1 deletion packages/api/src/business/rules-demarches/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,5 +154,5 @@
return undefined
}

return definition?.machine(titreTypeId, demarcheTypeId)
return definition?.machine(titreInfos)

Check failure on line 157 in packages/api/src/business/rules-demarches/definitions.ts

View workflow job for this annotation

GitHub Actions / api / unit-test

business/processes/titres-demarches-depot-create.test.ts > créer le dépot de la démarche > crée un dépot d’une ARM avec une demande faite

ReferenceError: titreInfos is not defined ❯ Module.machineFind business/rules-demarches/definitions.ts:157:30 ❯ Module.titreDemarcheDepotCheck business/processes/titres-demarches-depot-create.ts:53:30 ❯ business/processes/titres-demarches-depot-create.test.ts:21:41

Check failure on line 157 in packages/api/src/business/rules-demarches/definitions.ts

View workflow job for this annotation

GitHub Actions / api / unit-test

business/processes/titres-demarches-depot-create.test.ts > créer le dépot de la démarche > ne crée pas un dépot d’une ARM si sa demande est en construction

ReferenceError: titreInfos is not defined ❯ Module.machineFind business/rules-demarches/definitions.ts:157:30 ❯ Module.titreDemarcheDepotCheck business/processes/titres-demarches-depot-create.ts:53:30 ❯ business/processes/titres-demarches-depot-create.test.ts:21:41

Check failure on line 157 in packages/api/src/business/rules-demarches/definitions.ts

View workflow job for this annotation

GitHub Actions / api / unit-test

business/processes/titres-demarches-depot-create.test.ts > créer le dépot de la démarche > ne crée pas un dépot d’une ARM si déjà déposée

ReferenceError: titreInfos is not defined ❯ Module.machineFind business/rules-demarches/definitions.ts:157:30 ❯ Module.titreDemarcheDepotCheck business/processes/titres-demarches-depot-create.ts:53:30 ❯ business/processes/titres-demarches-depot-create.test.ts:21:41

Check failure on line 157 in packages/api/src/business/rules-demarches/definitions.ts

View workflow job for this annotation

GitHub Actions / api / unit-test

business/processes/titres-demarches-public-update.test.ts > public des démarches d'un titre > met à jour la publicité d'une démarche

ReferenceError: titreInfos is not defined ❯ Module.machineFind business/rules-demarches/definitions.ts:157:30 ❯ Module.titreDemarchePublicFind business/rules/titre-demarche-public-find.ts:171:21 ❯ Module.titresDemarchesPublicUpdate business/processes/titres-demarches-public-update.ts:35:53 ❯ business/processes/titres-demarches-public-update.test.ts:21:42

Check failure on line 157 in packages/api/src/business/rules-demarches/definitions.ts

View workflow job for this annotation

GitHub Actions / api / unit-test

business/processes/titres-etapes-ordre-update.test.ts > ordre des étapes > met à jour l'ordre de deux étapes

ReferenceError: titreInfos is not defined ❯ Module.machineFind business/rules-demarches/definitions.ts:157:30 ❯ Module.titreEtapesSortAscByDate business/utils/titre-etapes-sort.ts:19:19 ❯ Module.titresEtapesOrdreUpdateVisibleForTesting business/processes/titres-etapes-ordre-update.ts:40:22 ❯ business/processes/titres-etapes-ordre-update.test.ts:77:39

Check failure on line 157 in packages/api/src/business/rules-demarches/definitions.ts

View workflow job for this annotation

GitHub Actions / api / unit-test

business/rules/titre-demarche-public-find.test.ts > publicité d'une démarche > une démarche sans étape n'est pas publique

ReferenceError: titreInfos is not defined ❯ Module.machineFind business/rules-demarches/definitions.ts:157:30 ❯ Module.titreDemarchePublicFind business/rules/titre-demarche-public-find.ts:171:21 ❯ business/rules/titre-demarche-public-find.test.ts:28:7

Check failure on line 157 in packages/api/src/business/rules-demarches/definitions.ts

View workflow job for this annotation

GitHub Actions / api / unit-test

business/rules-demarches/definitions.test.ts > demarcheDefinitionFind retourne une machine

ReferenceError: titreInfos is not defined ❯ Module.machineFind business/rules-demarches/definitions.ts:157:30 ❯ business/rules-demarches/definitions.test.ts:7:10

Check failure on line 157 in packages/api/src/business/rules-demarches/definitions.ts

View workflow job for this annotation

GitHub Actions / api / unit-test

business/rules-demarches/definitions.test.ts > demarcheDefinitionFind retourne une machine quand il n'y a pas d'étape

ReferenceError: titreInfos is not defined ❯ Module.machineFind business/rules-demarches/definitions.ts:157:30 ❯ business/rules-demarches/definitions.test.ts:11:10
}
14 changes: 13 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 @@ -21,8 +21,16 @@ export interface Etape {
contenu?: IContenu
paysId?: PaysId
surface?: number
isPremiereDemandePourMiseEnConcurrence?: boolean
}

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 Expand Up @@ -85,6 +93,10 @@ const toMachineEtape = (dbEtape: Omit<TitreEtapeForMachine, 'id' | 'ordre'>): Et
machineEtape.surface = dbEtape.surface
}

if (dbEtape.isPremiereDemandePourMiseEnConcurrence !== null) {
machineEtape.isPremiereDemandePourMiseEnConcurrence = dbEtape.isPremiereDemandePourMiseEnConcurrence
}

return machineEtape
}

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
Loading