Instaure un nouveau moteur pour le choix du status

Le but est d'arriver au résultat en un minimum de question. Le moteur pose les questions les plus importantes (qui départagent le plus de status) en premier. Si la question peut aboutir à une absence de status concordant, elle n'est pas posée. Le moteur permet aussi de commencer par n'importe quelle question. Dans le cadre du référencement direct, cela signifie que l'on peut arriver sur la page liability par exemple via une recherche / lien et continuer à partir de ce point d'entrée.
pull/294/head
Johan Girod 2018-07-23 15:21:00 +02:00 committed by Mael
parent 336266b2b3
commit fce23e51a4
22 changed files with 600 additions and 253 deletions

View File

@ -45,6 +45,7 @@
"reduce-reducers": "^0.1.2",
"redux": "^3.7.2",
"redux-form": "^7.4.2",
"redux-thunk": "^2.3.0",
"reselect": "^3.0.1",
"screenfull": "^3.3.2"
},

View File

@ -7,6 +7,7 @@ import { Provider } from 'react-redux'
import { Router } from 'react-router-dom'
import reducers from 'Reducers/rootReducer'
import { applyMiddleware, compose, createStore } from 'redux'
import thunk from 'redux-thunk'
import computeThemeColours from 'Ui/themeColours'
import trackDomainActions from './middlewares/trackDomainActions'
import {
@ -43,10 +44,7 @@ let initialStore = {
}
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
let enhancer = composeEnhancers(applyMiddleware(trackDomainActions(tracker)))
let store = createStore(reducers, initialStore, enhancer)
persistSimulation(store)
if (process.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker
@ -61,17 +59,27 @@ if (process.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
}
export default class Layout extends PureComponent {
state = {
history: createHistory({
constructor(props) {
super(props)
this.history = createHistory({
basename: process.env.NODE_ENV === 'production' ? '' : this.props.basename
})
const storeEnhancer = composeEnhancers(
applyMiddleware(
// Allows us to painlessly do route transition in action creators
thunk.withExtraArgument(this.history),
trackDomainActions(tracker)
)
)
this.store = createStore(reducers, initialStore, storeEnhancer)
persistSimulation(this.store)
}
render() {
return (
<Provider store={store}>
<Provider store={this.store}>
<TrackerProvider value={tracker}>
<I18nextProvider i18n={i18next}>
<Router history={tracker.connectToHistory(this.state.history)}>
<Router history={tracker.connectToHistory(this.history)}>
<>{this.props.children}</>
</Router>
</I18nextProvider>

View File

@ -2,34 +2,47 @@
import type {
ChooseCompanyLiabilityAction,
CompanyLiability,
CompanyHaveMultipleAssociateAction,
CompanyHaveMultipleAssociatesAction,
DirectorStatus,
CompanyIsMicroenterpriseAction,
DefineDirectorStatusAction
} from 'Types/companyStatusTypes'
import type { RouterHistory } from 'react-router'
import { nextQuestionUrlSelector } from 'Selectors/companyStatusSelectors'
export function chooseCompanyLiability(
setup: CompanyLiability
): ChooseCompanyLiabilityAction {
return {
const thenGoToNextQuestion = actionCreator => (...args: any) => (
dispatch: any => void,
getState: () => any,
history: RouterHistory
) => {
dispatch(actionCreator(...args))
history.push(nextQuestionUrlSelector(getState()))
}
export const chooseCompanyLiability = thenGoToNextQuestion(
(setup: ?CompanyLiability): ChooseCompanyLiabilityAction => ({
type: 'CHOOSE_COMPANY_LEGAL_SETUP',
setup
}
}
})
)
export function defineDirectorStatus(
status: DirectorStatus
): DefineDirectorStatusAction {
return {
export const defineDirectorStatus = thenGoToNextQuestion(
(status: ?DirectorStatus): DefineDirectorStatusAction => ({
type: 'DEFINE_DIRECTOR_STATUS',
status
}
}
})
)
export function companyHaveMultipleAssociate(
multipleAssociate: boolean
): CompanyHaveMultipleAssociateAction {
return {
type: 'COMPANY_HAVE_MULTIPLE_ASSOCIATE',
multipleAssociate
}
}
export const companyHaveMultipleAssociates = thenGoToNextQuestion(
(multipleAssociates: ?boolean): CompanyHaveMultipleAssociatesAction => ({
type: 'COMPANY_HAVE_MULTIPLE_ASSOCIATES',
multipleAssociates
})
)
export const companyIsMicroenterprise = thenGoToNextQuestion(
(microenterprise: ?boolean): CompanyIsMicroenterpriseAction => ({
type: 'COMPANY_IS_MICROENTERPRISE',
microenterprise
})
)

View File

@ -32,12 +32,12 @@
border-color: rgb(41, 117, 209);
color: rgb(41, 117, 209);
background: linear-gradient(
45deg,
50deg,
rgba(39, 69, 195, 0.87) 5%,
rgba(41, 117, 209, 1) 50%,
rgba(255, 255, 255, 0.52) 55%
);
background-size: 250%;
background-size: 260%;
background-position-x: 99%;
}
.ui__.button:not(:disabled):hover,

View File

@ -17,8 +17,10 @@ function companyLegalStatus(
case 'DEFINE_DIRECTOR_STATUS':
return { ...state, directorStatus: action.status }
case 'COMPANY_HAVE_MULTIPLE_ASSOCIATE':
return { ...state, multipleAssociate: action.multipleAssociate }
case 'COMPANY_HAVE_MULTIPLE_ASSOCIATES':
return { ...state, multipleAssociates: action.multipleAssociates }
case 'COMPANY_IS_MICROENTERPRISE':
return { ...state, microenterprise: action.microenterprise }
}
return state
}

View File

@ -1,74 +1,150 @@
/* @flow */
import type {
State,
CompanyLegalStatus,
DirectorStatus
} from 'Types/companyStatusTypes'
import { map, whereEq } from 'ramda'
import type { State, CompanyLegalStatus } from 'Types/companyStatusTypes'
import {
add,
countBy,
difference,
filter,
map,
pick,
sortBy,
whereEq
} from 'ramda'
const LEGAL_STATUS_DETAILS: { [status: string]: CompanyLegalStatus } = {
Microenterprise: {
liability: 'SOLE_PROPRIETORSHIP',
directorStatus: 'SELF_EMPLOYED',
multipleAssociates: false,
microenterprise: true,
},
'Microenterprise (option EIRL)': {
liability: 'LIMITED_LIABILITY',
directorStatus: 'SELF_EMPLOYED',
multipleAssociates: false,
microenterprise: true
},
EI: {
liability: 'SOLE_PROPRIETORSHIP',
directorStatus: 'SELF_EMPLOYED',
multipleAssociate: false
multipleAssociates: false,
microenterprise: false
},
EURL: {
liability: 'LIMITED_LIABILITY',
directorStatus: 'SELF_EMPLOYED',
multipleAssociate: false
multipleAssociates: false,
microenterprise: false
},
EIRL: {
liability: 'LIMITED_LIABILITY',
directorStatus: 'SELF_EMPLOYED',
multipleAssociate: false
multipleAssociates: false,
microenterprise: false
},
SARL: {
liability: 'LIMITED_LIABILITY',
directorStatus: 'SELF_EMPLOYED',
multipleAssociate: true
multipleAssociates: true,
microenterprise: false
},
SAS: {
liability: 'LIMITED_LIABILITY',
directorStatus: 'SALARIED',
multipleAssociate: true
multipleAssociates: true,
microenterprise: false
},
SA: {
liability: 'LIMITED_LIABILITY',
directorStatus: 'SALARIED',
multipleAssociate: true
multipleAssociates: true,
microenterprise: false
},
SNC: {
liability: 'SOLE_PROPRIETORSHIP',
directorStatus: 'SELF_EMPLOYED',
multipleAssociate: true
multipleAssociates: true,
microenterprise: false
},
SASU: {
liability: 'LIMITED_LIABILITY',
directorStatus: 'SELF_EMPLOYED',
multipleAssociate: false
directorStatus: 'SALARIED',
multipleAssociates: false,
microenterprise: false
}
}
export type LegalStatus = $Keys<typeof LEGAL_STATUS_DETAILS>
const possibleStatus = (
companyLegalStatus: CompanyLegalStatus
): { [LegalStatus]: boolean } =>
// $FlowFixMe
map(whereEq(companyLegalStatus), LEGAL_STATUS_DETAILS)
map(
// $FlowFixMe
whereEq(filter(x => x !== null, companyLegalStatus)),
LEGAL_STATUS_DETAILS
)
export const possibleStatusSelector = (state: {
inFranceApp: State
}): { [LegalStatus]: boolean } =>
possibleStatus(state.inFranceApp.companyLegalStatus)
export const disabledDirectorStatusSelector = (state: {
type Question = $Keys<CompanyLegalStatus>
const QUESTION_LIST: Array<Question> = Object.keys(LEGAL_STATUS_DETAILS.SA);
export const nextQuestionSelector = (state: {
inFranceApp: State
}): Array<DirectorStatus> =>
['SALARIED', 'SELF_EMPLOYED'].filter(directorStatus =>
Object.values(
possibleStatus({
...state.inFranceApp.companyLegalStatus,
directorStatus
})
).every(x => x === false)
}): ?Question => {
const companyLegalStatus = state.inFranceApp.companyLegalStatus
const questionAnswered = Object.keys(companyLegalStatus)
const possibleStatusList = pick(
Object.keys(filter(Boolean, possibleStatus(companyLegalStatus))),
LEGAL_STATUS_DETAILS
)
const unansweredQuestions = difference(QUESTION_LIST, questionAnswered)
const shannonEntropyByQuestion = unansweredQuestions.map(question => {
const answerPopulation = Object.values(possibleStatusList).map(
// $FlowFixMe
status => status[question]
)
const frequencyOfAnswers = Object.values(
countBy(x => x, answerPopulation)
).map(
numOccurrence =>
// $FlowFixMe
numOccurrence / answerPopulation.length
)
const shannonEntropy = -frequencyOfAnswers
.map(p => p * Math.log2(p))
// $FlowFixMe
.reduce(add, 0)
return [question, shannonEntropy]
})
const sortedPossibleNextQuestions = sortBy(
([, entropy]) => -entropy,
shannonEntropyByQuestion.filter(([, entropy]) => entropy !== 0)
).map(([question]) => question)
if (sortedPossibleNextQuestions.length === 0) {
return null
}
return sortedPossibleNextQuestions[0]
}
export const nextQuestionUrlSelector = (state: { inFranceApp: State }) => {
const nextQuestion = nextQuestionSelector(state)
if (!nextQuestion) {
return '/register/pick-legal-status'
}
return (
'/register/' +
nextQuestion
.replace(/[^a-zA-Z0-9]+/g, '-')
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
.replace(/([a-z])([A-Z])/g, '$1-$2')
.replace(/([0-9])([^0-9])/g, '$1-$2')
.replace(/([^0-9])([0-9])/g, '$1-$2')
.replace(/-+/g, '-')
.toLowerCase()
)
}

View File

@ -2,5 +2,46 @@ import React from 'react'
import { render } from 'react-dom'
import App from './App'
// Avoid `console` errors in browsers that lack a console.
;(function() {
var method
var noop = function() {}
var methods = [
'assert',
'clear',
'count',
'debug',
'dir',
'dirxml',
'error',
'exception',
'group',
'groupCollapsed',
'groupEnd',
'info',
'log',
'markTimeline',
'profile',
'profileEnd',
'table',
'time',
'timeEnd',
'timeStamp',
'trace',
'warn'
]
var length = methods.length
var console = (window.console = window.console || {})
while (length--) {
method = methods[length]
// Only stub undefined methods.
if (!console[method]) {
console[method] = noop
}
}
})()
let anchor = document.querySelector('#js')
render(<App />, anchor)

View File

@ -1,27 +1,14 @@
/* @flow */
import { defineDirectorStatus } from 'Actions/companyStatusActions'
import { equals } from 'ramda'
import React from 'react'
import { connect } from 'react-redux'
import { disabledDirectorStatusSelector } from 'Selectors/companyStatusSelectors'
import { SkipButton } from 'Ui/Button'
import type { DirectorStatus } from 'Types/companyStatusTypes'
import type { RouterHistory } from 'react-router'
type Props = {
history: RouterHistory,
defineDirectorStatus: DirectorStatus => void,
disabledDirectorStatus: Array<DirectorStatus>
defineDirectorStatus: (?DirectorStatus) => void
}
const goToNextStep = (history: RouterHistory) => {
history.push('/register/set-legal-status')
}
const DefineDirectorStatus = ({
history,
defineDirectorStatus,
disabledDirectorStatus
}: Props) => (
const DefineDirectorStatus = ({ defineDirectorStatus }: Props) => (
<>
<h2>Defining the director&apos;s status </h2>
<p>
@ -47,39 +34,27 @@ const DefineDirectorStatus = ({
professional income as reported to the tax authorities.
</li>
</ul>
{!!disabledDirectorStatus.length && (
<p>
Because of your previous choices, you only have the following
possibility for the director status:
</p>
)}
<div className="ui__ answer-group">
{!disabledDirectorStatus.find(equals('SALARIED')) && (
<button
className="ui__ button"
onClick={() => {
defineDirectorStatus('SALARIED')
goToNextStep(history)
}}>
Salaried
</button>
)}
{!disabledDirectorStatus.find(equals('SELF-EMPLOYED')) && (
<button
className="ui__ button"
onClick={() => {
defineDirectorStatus('SELF_EMPLOYED')
goToNextStep(history)
}}>
Self-employed
</button>
)}
<SkipButton onClick={() => goToNextStep(history)} />
<button
className="ui__ button"
onClick={() => {
defineDirectorStatus('SALARIED')
}}>
Salaried
</button>
<button
className="ui__ button"
onClick={() => {
defineDirectorStatus('SELF_EMPLOYED')
}}>
Self-employed
</button>
<SkipButton onClick={() => defineDirectorStatus(null)} />
</div>
</>
)
export default connect(
state => ({ disabledDirectorStatus: disabledDirectorStatusSelector(state) }),
null,
{ defineDirectorStatus }
)(DefineDirectorStatus)

View File

@ -1,12 +1,14 @@
/* @flow */
import React from 'react'
import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
import { nextQuestionUrlSelector } from 'Selectors/companyStatusSelectors'
import type { Match } from 'react-router'
type Props = {
match: Match
match: Match,
nextQuestionUrl: string
}
const CreateMyCompany = ({ match }: Props) => (
const CreateMyCompany = ({ match, nextQuestionUrl }: Props) => (
<>
<h1 className="question__title">Register a company</h1>
<Link className="ui__ link-button" to="/register/find">
@ -21,7 +23,7 @@ const CreateMyCompany = ({ match }: Props) => (
</p>
{match.isExact && (
<div className="ui__ answer-group">
<Link className="ui__ button" to={match.path + '/choose-liability'}>
<Link className="ui__ button" to={nextQuestionUrl}>
Choose the legal status
</Link>
<Link to={'/social-security'} className="ui__ skip-button">
@ -32,4 +34,7 @@ const CreateMyCompany = ({ match }: Props) => (
</>
)
export default CreateMyCompany
export default connect(
state => ({ nextQuestionUrl: nextQuestionUrlSelector(state) }),
null
)(CreateMyCompany)

View File

@ -3,19 +3,15 @@ import { chooseCompanyLiability } from 'Actions/companyStatusActions'
import React from 'react'
import { connect } from 'react-redux'
import { SkipButton } from 'Ui/Button'
import type { Match, RouterHistory } from 'react-router'
import type { Match } from 'react-router'
import type { CompanyLiability } from 'Types/companyStatusTypes'
type Props = {
match: Match,
history: RouterHistory,
chooseCompanyLiability: CompanyLiability => void
chooseCompanyLiability: (?CompanyLiability) => void
}
const goToNextStep = (history: RouterHistory) => {
history.push('/register/number-of-associate')
}
const Liability = ({ chooseCompanyLiability, history }: Props) => (
const Liability = ({ chooseCompanyLiability }: Props) => (
<>
<h2>Choosing the liability </h2>
<p>
@ -40,7 +36,6 @@ const Liability = ({ chooseCompanyLiability, history }: Props) => (
<div className="ui__ answer-group">
<button
onClick={() => {
goToNextStep(history)
chooseCompanyLiability('SOLE_PROPRIETORSHIP')
}}
className="ui__ button">
@ -49,12 +44,11 @@ const Liability = ({ chooseCompanyLiability, history }: Props) => (
<button
onClick={() => {
chooseCompanyLiability('LIMITED_LIABILITY')
goToNextStep(history)
}}
className="ui__ button">
Limited liability
</button>
<SkipButton onClick={() => goToNextStep(history)} />
<SkipButton onClick={() => chooseCompanyLiability(null)} />
</div>
{/* this is an economic activity conducted by a single natural person, in his own name ; */}
{/* Company : This is an economic activity conducted by a single partner - single member company with limited liability (EURL) - or several partners (limited liability company (SARL), public limited company (SA), simplified joint-stock company (SAS)...). */}
@ -63,7 +57,5 @@ const Liability = ({ chooseCompanyLiability, history }: Props) => (
export default connect(
null,
{
chooseCompanyLiability
}
{ chooseCompanyLiability }
)(Liability)

View File

@ -13,6 +13,7 @@ type Props = {
setMainStatus: LegalStatus => void
}
const StatusButton = ({ status }: { status: LegalStatus }) => (
<Link to={`/register/register-${status}`} className="ui__ button">
Create {status}
@ -20,25 +21,16 @@ const StatusButton = ({ status }: { status: LegalStatus }) => (
)
const SetMainStatus = ({ history, possibleStatus }: Props) => {
const atLeastOneStatus = Object.values(possibleStatus).some(x => x)
return (
const uniqStatus = (Object.values(possibleStatus).filter(Boolean).length === 1);
return (
<>
<h2>Choosing a legal status</h2>
{atLeastOneStatus ? (
<p>
Based on your previous answers, you can choose between the following
statuses:
</p>
) : (
<p>
{' '}
We didn&apos;t find any status matching your need. You can go back and
change your needs, or choose a status manually from the following
list:
</p>
)}
<h2>Your legal status</h2>
{uniqStatus? <p>The following status seems to be the perfect match for your need:</p> : <p>
Based on your previous answers, you can choose between the following statuses:
</p>}
<ul>
{(!atLeastOneStatus || possibleStatus.EI) && (
{possibleStatus.EI && (
<li>
<strong>
EI - Entreprise individuelle (Individual business):{' '}
@ -48,7 +40,7 @@ const SetMainStatus = ({ history, possibleStatus }: Props) => {
wealth are one.
</li>
)}
{(!atLeastOneStatus || possibleStatus.EIRL) && (
{possibleStatus.EIRL && (
<li>
<strong>
EIRL - Entrepreneur individuel à responsabilité limitée
@ -58,7 +50,7 @@ const SetMainStatus = ({ history, possibleStatus }: Props) => {
heritage necessary for the activity.
</li>
)}
{(!atLeastOneStatus || possibleStatus.EURL) && (
{possibleStatus.EURL && (
<li>
<strong>
EURL - Entreprise unipersonnelle à responsabilité limitée (Limited
@ -68,7 +60,7 @@ const SetMainStatus = ({ history, possibleStatus }: Props) => {
its contribution to the capital.
</li>
)}
{(!atLeastOneStatus || possibleStatus.SARL) && (
{possibleStatus.SARL && (
<li>
<strong>
SARL - Société à responsabilité limitée (Limited corporation):{' '}
@ -78,7 +70,7 @@ const SetMainStatus = ({ history, possibleStatus }: Props) => {
capital is freely fixed in the statutes.
</li>
)}
{(!atLeastOneStatus || possibleStatus.SAS) && (
{possibleStatus.SAS && (
<li>
<strong>
SAS - Société par actions simplifiées (Simplified joint stock
@ -89,7 +81,7 @@ const SetMainStatus = ({ history, possibleStatus }: Props) => {
the statutes.
</li>
)}
{(!atLeastOneStatus || possibleStatus.SASU) && (
{possibleStatus.SASU && (
<li>
<strong>
SASU - Société par action simplifiée unipersonnelle (Simplified
@ -99,24 +91,36 @@ const SetMainStatus = ({ history, possibleStatus }: Props) => {
capital. The minimum capital is freely fixed in the statutes.
</li>
)}
{(!atLeastOneStatus || possibleStatus.SA) && (
{possibleStatus.SA && (
<li>
<strong>SA - Société anonyme (Anonymous company):</strong>Company
composed of at least 2 shareholders if it is not listed.
composed of at least 2 shareholders. The only status that allows you to be listed on the stock exchange. The minimum share capital is 37.000.
</li>
)}
{(!atLeastOneStatus || possibleStatus.SNC) && (
{possibleStatus.SNC && (
<li>
<strong>SNC - Société en nom collectif (Partnership):</strong>The
partners are liable indefinitely and severally for the debts of the
company.
</li>
)}
{possibleStatus['Microenterprise (option EIRL)'] && (
<li>
<strong>Microenterprise (option EIRL):</strong> The micro-enterprise is a sole proprietorship company, subject to a flat-rate scheme for the calculation of taxes and the payment of social security contributions. With the EIRL option, you have limited liability on your losses.
</li>
)}
{possibleStatus.Microenterprise && (
<li>
<strong>Microenterprise:</strong> The micro-enterprise is a sole proprietorship subject to a flat-rate scheme for the calculation of taxes and the payment of social security contributions.
</li>
)}
</ul>
<div className="ui__ answer-group">
{/* $FlowFixMe */}
{(Object.entries(possibleStatus): Array<[LegalStatus, boolean]>)
.filter(([, statusIsVisible]) => statusIsVisible || !atLeastOneStatus)
.filter(([, statusIsVisible]) => statusIsVisible)
.map(([status]) => (
<StatusButton key={status} status={status} history={history} />
))}

View File

@ -0,0 +1,43 @@
/* @flow */
import { companyIsMicroenterprise } from 'Actions/companyStatusActions'
import React from 'react'
import { connect } from 'react-redux'
import { SkipButton } from 'Ui/Button'
type Props = {
companyIsMicroenterprise: (?boolean) => void
}
const Microenterprise = ({ companyIsMicroenterprise }: Props) => (
<>
<h2>Microenterprise or Individual Business</h2>
<p>
The Micro entreprise is a simplified scheme of declaration and payment, whose tax and social contributions are based on the turnover achieved each month. Available for
companies whose annual turnover does not exceed (for the past year) 70 000 for services providers or 170 000 for micro-entrepreneurs whose main activity is the sale of goods, catering or the provision of housing.
</p><p>This is a interesting choice if you do not need lot of capital for your activity, you plan it to be small, and you want the minimum amount of paperwork to get started. </p>
<p>For all other case, it is advised to choose the standard status, which is Individual Business.</p>
<div className="ui__ answer-group">
<button
onClick={() => {
companyIsMicroenterprise(true)
}}
className="ui__ button">
Microenterprise
</button>
<button
onClick={() => {
companyIsMicroenterprise(false)
}}
className="ui__ button">
Individual Business
</button>
<SkipButton onClick={() => companyIsMicroenterprise(null)} />
</div>
</>
)
export default connect(
null,
{ companyIsMicroenterprise }
)(Microenterprise)

View File

@ -1,23 +1,14 @@
/* @flow */
import { companyHaveMultipleAssociate } from 'Actions/companyStatusActions'
import { companyHaveMultipleAssociates } from 'Actions/companyStatusActions'
import React from 'react'
import { connect } from 'react-redux'
import { SkipButton } from 'Ui/Button'
import type { RouterHistory } from 'react-router'
type Props = {
history: RouterHistory,
companyHaveMultipleAssociate: boolean => void
companyHaveMultipleAssociates: (?boolean) => void
}
const goToNextStep = (history: RouterHistory) => {
history.push('/register/define-director-status')
}
const NumberOfAssociate = ({
history,
companyHaveMultipleAssociate
}: Props) => (
const NumberOfAssociate = ({ companyHaveMultipleAssociates }: Props) => (
<>
<h2>Number of associates </h2>
<p>
@ -28,26 +19,24 @@ const NumberOfAssociate = ({
<div className="ui__ answer-group">
<button
onClick={() => {
companyHaveMultipleAssociate(false)
goToNextStep(history)
companyHaveMultipleAssociates(false)
}}
className="ui__ button">
Only one associate
</button>
<button
onClick={() => {
companyHaveMultipleAssociate(true)
goToNextStep(history)
companyHaveMultipleAssociates(true)
}}
className="ui__ button">
Multiple partners
</button>
<SkipButton onClick={() => goToNextStep(history)} />
<SkipButton onClick={() => companyHaveMultipleAssociates(null)} />
</div>
</>
)
export default connect(
null,
{ companyHaveMultipleAssociate }
{ companyHaveMultipleAssociates }
)(NumberOfAssociate)

View File

@ -1,75 +1,64 @@
/* @flow */
import Checklist from 'Components/Checklist'
import React from 'react'
import { Link } from 'react-router-dom'
import siret from './siret.jpg'
import type { Match } from 'react-router'
export default (match: Match) =>
Checklist({
name: 'register',
title: `Checklist to register a ${match.params.status || ''}`,
subtitle: `
This checklist will guide you thoughout all the necessary steps to
register your company with the French administration.
`,
items: {
legalStatus: 'Choose the legal status',
corporateName: (
<p>
Find a corporate name (<em>raison sociale</em>, the legal name of your
company)
</p>
),
tradeName: 'Find a trade name (for commercial purposes)',
space: 'Find a space (or work at home)',
registerCfe: (
<span>
Register your company online on{' '}
<a target="_blank" href="https://www.guichet-entreprises.fr/en/">
Guichet-entreprises.fr (english)
</a>
</span>
),
newspaper: `Have the company's creation published in
a newspaper of legal announcements such as the Bodacc (Bulletin officiel
des annonces civiles et commerciales)`,
bankAccount:
'Open a business bank account and follow the capital deposit procedure if needed',
accountant: 'Choose a certified accountant',
insurance: 'Check out needs of professional insurance'
},
conclusion: (
<>
<p>
Once your business has been officially registered, you will receive :
</p>
<ul>
<li>your Siren number, which identifies your company ;</li>
<li>
the Siret number, which identifies each place of business operated
by the same company.
</li>
</ul>
<img src={siret} alt="Siret and siren number" />
<p>
It also assigns the APE code for the business sector to which your
company or you as a self-employed worker belong. The APE code is used
to classify your companys main operations in relation to the french
business nomenclature system (« NAF » code). It also determines the
applicable collective agreement as well as the industrial accident
rate in the field to which you or your company belong.
</p>
<p>
Now that you have a properly registered company, the next steps is to{' '}
<strong>hire your first employee</strong>
</p>
<div style={{ textAlign: 'center' }}>
<Link className="ui__ button" to="/social-security">
Simulate hiring cost in France
</Link>
</div>
</>
)
})
import type { Match, RouterHistory } from 'react-router'
type Props = {
history: RouterHistory,
match: Match
}
const Register = ({ match, history }: Props) => (
<>
<h1>Create a {match.params.status} </h1>
<p>
<Link to="/register">
Not sure about this status? Take our guide to help you choose.
</Link>{' '}
</p>
<p>
Register your company to the French administration is the first thing to
do. It can be done online with the following data :
</p>
<ul>
<li>
<strong>The corporate name</strong>, also called "raison sociale" in
french, is the legal name of your company, written on all of your
administrative papers. It can be different from the trade name (used for
commercial purpose).
</li>
<li>
<strong>The corporate purpose of the company</strong>, also called
"object social" is a short phrase describing the activity of your
company. As it is legally binding it must be composed with care,
possibly with the help of a lawyer.
</li>
<li>
<strong>The social security number of the director</strong>. In case you
don't have yet a french social security number...
</li>
<li>
<strong>The address</strong>, the physical space where your company will
be incorporated. In certain areas, you can benefit from substantial
government aid (exemption from charges, taxes, etc.).
</li>
</ul>
<p>
If you don't know where your going to open your company, you can discover
the French territories in our <a>incoporation simulator</a>.
</p>
{/* <p>If the company director is not part of the EU, you'll need a specific visa https://www.economie.gouv.fr/entreprises/etranger-comment-creer-votre-entreprise-france </p> */}
<p style={{ textAlign: 'right' }}>
<a
onClick={() => history.push('/register/registration-pending')}
className="ui__ button"
href="https://translate.google.com/translate?depth=1&hl=en&rurl=translate.google.com&sl=fr&sp=nmt4&tl=en&u=https://www.guichet-entreprises.fr/en/how-to-create-your-business/"
rel="noopener noreferrer"
target="_blank">
Register my company online
</a>
<Link to={'/social-security'} className="ui__ skip-button">
Do it later
</Link>
</p>
</>
)
export default Register

View File

@ -0,0 +1,74 @@
import React from 'react'
import { Link } from 'react-router-dom'
import siret from './siret.jpg'
const DuringRegistration = () => (
<>
<h1>Registration pending</h1>
<p>
<a>If you have trouble completing your application, we can help.</a>
</p>
<p>
While your application is being processed, you can focus on the following
tasks:{' '}
</p>
<ul className="ui__ no-bullet">
<li>
<label>
<input type="checkbox" />Open a business bank account and follow the
capital deposit procedure if needed
</label>
</li>
<li>
<label>
<input type="checkbox" />Choose a certified accountant
</label>
</li>
<li>
<label>
<input type="checkbox" />Check out needs of professional insurance
</label>
</li>
</ul>
<p>
You can also{' '}
<Link to="/social-security">
learn more about social security system and simulate your first employee
</Link>
</p>
<h2>Application status</h2>
<p>Once your business has been officially registered, you will receive:</p>
<ul>
<li>
<strong>Your Siret number</strong>
, which identifies your company
</li>
<li>
<strong>Your APE code</strong>
, which defines your business sector
</li>
<li>
<strong>Your K-bis extract</strong>
, which certifies that your company is properly registrated
</li>
</ul>
<a className="ui__ button">I've received my SIRET number</a>
<h3>Siren and Siret</h3>
<p>
The Siren number identifies your company while the Siret number identifies
each place of business operated by the same company.
</p>
<img src={siret} alt="Siret and siren number" />
<h3>APE Code</h3>
<p>
The APE code for the business sector to which your company belong. The APE
code is used to classify your companys main operations in relation to the
french business nomenclature system (« NAF » code). It also determines the
applicable collective agreement as well as the industrial accident rate in
the field to which you or your company belong.
</p>
<h3>Kbis extract</h3>
</>
)
export default DuringRegistration

View File

@ -7,6 +7,8 @@ import Find from './Find'
import Home from './Home'
import Liability from './Liability'
import MainStatus from './MainStatus'
import Microenterprise from './Microenterprise'
import RegistrationPending from './RegistrationPending'
import NumberOfAssociate from './NumberOfAssociate'
import Register from './Register'
@ -16,8 +18,9 @@ const CreateMyCompany = ({ match, location }) => (
<Switch>
<Route
path={match.path + '/register-:status'}
component={Register(match)}
component={Register}
/>
<Route path={match.path + '/registration-pending'} component={RegistrationPending} />
<Route path={match.path + '/find'} component={Find} />
<Route path={match.path} component={Home} />
@ -42,7 +45,7 @@ const CreateMyCompany = ({ match, location }) => (
{style => (
<Switch location={location}>
<Route
path={match.path + '/choose-liability'}
path={match.path + '/liability'}
render={props => (
<animated.div style={style}>
<Liability {...props} />
@ -50,7 +53,7 @@ const CreateMyCompany = ({ match, location }) => (
)}
/>
<Route
path={match.path + '/define-director-status'}
path={match.path + '/director-status'}
render={props => (
<animated.div style={style}>
<DefineDirectorStatus {...props} />
@ -58,7 +61,15 @@ const CreateMyCompany = ({ match, location }) => (
)}
/>
<Route
path={match.path + '/number-of-associate'}
path={match.path + '/microenterprise'}
render={props => (
<animated.div style={style}>
<Microenterprise {...props} />
</animated.div>
)}
/>
<Route
path={match.path + '/multiple-associates'}
render={props => (
<animated.div style={style}>
<NumberOfAssociate {...props} />
@ -66,7 +77,7 @@ const CreateMyCompany = ({ match, location }) => (
)}
/>
<Route
path={match.path + '/set-legal-status'}
path={match.path + '/pick-legal-status'}
render={props => (
<animated.div style={style}>
<MainStatus {...props} />

View File

@ -21,11 +21,16 @@ class Hiring extends Component<Props, {}> {
designed to ensure the{' '}
<strong>general welfare of its people</strong>.
</p>
<p>
This easy access to health care and other services ensures that
companies can put healthy, highly skilled, and productive
employees to work in an attractive market in the heart of Europe.
</p>
<p>
As soon as you declare and pay your employees, you automatically
entitle them to all of Frances health, maternity, disability, old
age, unemployment, occupational accidents and occupational illness
insurance programs.
entitle them to the general scheme of French Social Security
(health, maternity, disability, old age, occupational illness,
accident at work) and unemployment insurance.
</p>
<div style={{ display: 'flex', alignItems: 'center' }}>
<iframe

View File

@ -3,19 +3,25 @@
export type CompanyLiability = 'LIMITED_LIABILITY' | 'SOLE_PROPRIETORSHIP'
export type ChooseCompanyLiabilityAction = {
type: 'CHOOSE_COMPANY_LEGAL_SETUP',
setup: CompanyLiability
setup: ?CompanyLiability
}
export type DirectorStatus = 'SALARIED' | 'SELF_EMPLOYED'
export type DefineDirectorStatusAction = {
type: 'DEFINE_DIRECTOR_STATUS',
status: DirectorStatus
status: ?DirectorStatus
}
export type CompanyHaveMultipleAssociateAction = {
type: 'COMPANY_HAVE_MULTIPLE_ASSOCIATE',
multipleAssociate: boolean
export type CompanyHaveMultipleAssociatesAction = {
type: 'COMPANY_HAVE_MULTIPLE_ASSOCIATES',
multipleAssociates: ?boolean
}
export type CompanyIsMicroenterpriseAction = {
type: 'COMPANY_IS_MICROENTERPRISE',
microenterprise: ?boolean
}
export type ChangeChecklistItemAction = {
@ -32,9 +38,15 @@ export type SaveExistingCompanyDetailsAction = {
export type State = {|
+companyLegalStatus: {
+liability?: CompanyLiability,
+directorStatus?: DirectorStatus,
+multipleAssociate?: boolean
/*
Note on the meanings of null / undefined value:
If the key exists and the value is null, the question have been asked, but skipped by the user.
If the key does not exists, the question still hasn't been asked.
*/
+liability?: ?CompanyLiability,
+directorStatus?: ?DirectorStatus,
+multipleAssociates?: ?boolean,
+microenterprise?: ?boolean
},
+existingCompanyDetails: ?{ [string]: string },
+checklists: {
@ -48,6 +60,7 @@ export type CompanyLegalStatus = $PropertyType<State, 'companyLegalStatus'>
export type Action =
| ChooseCompanyLiabilityAction
| DefineDirectorStatusAction
| CompanyHaveMultipleAssociateAction
| CompanyIsMicroenterpriseAction
| CompanyHaveMultipleAssociatesAction
| SaveExistingCompanyDetailsAction
| ChangeChecklistItemAction

View File

@ -1,4 +1,5 @@
/* @flow */
import { map } from 'ramda'
export let capitalise0 = (name: string) => name[0].toUpperCase() + name.slice(1)
@ -45,3 +46,13 @@ export function isIE() {
) != null)
)
}
export const mapDispatchWithRouter = (actionCreators: Object) => (
dispatch: (...any) => void,
ownProps: Object
) =>
map(
actionCreator => (...args) =>
dispatch(actionCreator(...args, ownProps.router)),
actionCreators
)

View File

@ -0,0 +1,91 @@
/* @flow */
import { expect } from 'chai'
import { nextQuestionSelector } from 'Selectors/companyStatusSelectors'
const state = companyLegalStatus => ({
inFranceApp: {
companyLegalStatus,
existingCompanyDetails: null,
checklists: { register: {}, hire: {} }
}
})
describe('company status selectors', function() {
describe('nextQuestionSelector', function() {
it('should return null there is only one status possible', () => {
const nextQuestion = nextQuestionSelector(
state({
liability: 'SOLE_PROPRIETORSHIP',
directorStatus: 'SELF_EMPLOYED',
multipleAssociates: true
})
)
expect(nextQuestion).to.be.equal(null)
})
it('should not return null if no questions have been answered yet', () => {
const nextQuestion = nextQuestionSelector(state({}))
expect(nextQuestion).not.to.be.equal(null)
})
it('should return null if all the questions have been answered', () => {
const nextQuestion = nextQuestionSelector(
state({
liability: null,
directorStatus: null,
microenterprise: null,
multipleAssociates: null
})
)
expect(nextQuestion).to.be.equal(null)
})
it('should always return a question that have not been answered yet', () => {
let nextQuestion = nextQuestionSelector(
state({
directorStatus: null,
multipleAssociates: null
})
)
expect(['directorStatus', 'multipleAssociates']).not.to.contain(
nextQuestion
)
nextQuestion = nextQuestionSelector(
state({
directorStatus: 'SALARIED',
liability: 'LIMITED_LIABILITY'
})
)
expect(['directorStatus', 'liability']).not.to.contain(nextQuestion)
nextQuestion = nextQuestionSelector(
state({
multipleAssociates: true,
liability: 'LIMITED_LIABILITY'
})
)
expect(['multipleAssociates', 'liability']).not.to.contain(nextQuestion)
})
it('should not return a question which can lead to no matching status', () => {
const nextQuestion = nextQuestionSelector(
state({
liability: 'SOLE_PROPRIETORSHIP',
multipleAssociates: null,
microenterprise: null,
})
)
expect(nextQuestion).to.be.equal(null)
})
it('should return a question if it can help to shrink down the possibilities', () => {
const nextQuestion = nextQuestionSelector(
state({
liability: 'LIMITED_LIABILITY',
directorStatus: 'SALARIED'
})
)
expect(nextQuestion).not.to.be.equal(null)
})
it('should first return the question which convey the most information (which eliminates the most statuses ) ', () => {
const nextQuestion = nextQuestionSelector(state({}))
expect(nextQuestion).to.be.equal('multipleAssociates')
})
})
})

View File

@ -6851,6 +6851,10 @@ redux-form@^7.4.2:
prop-types "^15.6.1"
react-lifecycles-compat "^3.0.4"
redux-thunk@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
redux@^3.7.2:
version "3.7.2"
resolved "https://registry.yarnpkg.com/redux/-/redux-3.7.2.tgz#06b73123215901d25d065be342eb026bc1c8537b"