myhappywallet
Fullstack personal budgeting application for tracking income and expenses and calculating disposable income, delivered as a Simplon CDA capstone project. The stack covers a Node.js/TypeScript/Express/Prisma backend, a React frontend, GitLab CI, and OVH VPS deployment across 212 commits on 3 repositories.
🎯 Context and goals
- Design and build a personal budgeting application to track fixed charges/income and calculate a disposable income value.
- Set up an industrializable fullstack baseline: secure API, state-driven frontend, data validation, and API documentation.
- Cover the full project lifecycle: product framing (UX, story mapping, user stories), data modeling, CI/CD, and deployment.
🛠️ Deliverables
Projects
- cda-my-happy-wallet-backend
- cda-my-happy-wallet-frontend
Projet-00-chef-oeuvre-CDA(Project-00-chef-oeuvre/Projet-00-chef-oeuvre-CDA)
🧩 Design
The design phase is structured around product vision, personas, story mapping, user stories, UML, Merise, then implementation. From an engineering-documentation perspective, the report repository includes an advanced LaTeX pipeline (minted) and a dedicated JSX Python lexer.
- Projet-00-chef-oeuvre-CDA: product need formalization and functional scope definition (budget, real/fictive disposable income, goals). Source:
Project-00-chef-oeuvre/Projet-00-chef-oeuvre-CDA/Rapport-CDA-2021-2022-CAPAI_Andria.tex:475.
\section{P\textsc{résentation du chef d'oeuvre}}
Le produit/service proposé est une application "MyHappyWallet" ...
... ses \textbf{charges et sources de revenu} ... connaitre son \gls{ravr} ...
... atteindre ses \textbf{objectifs financiers} ...
Pour calculer son \gls{ravrg} , l'utilisateur doit être \textbf{enregistré}
... charges et revenus fixes ...
Le système renseigne à l'utilisateur un \textbf{bilan mensuel}
... montant (solde) du "Reste à vivre" ...
- Projet-00-chef-oeuvre-CDA: Agile/Scrum framing with short-iteration backlog prioritization. Source:
Project-00-chef-oeuvre/Projet-00-chef-oeuvre-CDA/Rapport-CDA-2021-2022-CAPAI_Andria.tex:629.
Dans ce chapitre est présenté les différents éléments utilisés pour la gestion du projet "MyHappyWallet".
... méthode \Gls{agile}.
Cette méthode implique 3 rôles dans l'équipe \Gls{scrum}.
\begin{enumerate}
\item Le \Gls{po}
\item \Gls{sm}
\item L'équipe de réalisation
\end{enumerate}
... la méthode \Gls{scrum} s'appuie sur des \Glspl{sprint} ...
- Projet-00-chef-oeuvre-CDA: LaTeX publishing-chain customization to properly support JSX syntax highlighting inside the technical report. Source:
Project-00-chef-oeuvre/Projet-00-chef-oeuvre-CDA/lexer.py:8.
# Use same tokens as `JavascriptLexer`, but with tags and attributes support
TOKENS = JavascriptLexer.tokens
TOKENS.update(
{
"jsx": [
(r"(<)(/?)(>)", bygroups(Punctuation, Punctuation, Punctuation)),
(r"(<)([\w]+)(\.?)", bygroups(Punctuation, Name.Tag, Punctuation), "tag"),
(r"(<)(/)([\w]+)(>)", bygroups(Punctuation, Punctuation, Name.Tag, Punctuation)),
],
"tag": [
(r"([\w]+\s*)(=)(\s*)", bygroups(Name.Attribute, Operator, Text), "attr"),
],
}
)
💻 Development
- cda-my-happy-wallet-backend: API bootstrap architecture using middleware pipeline (parsing, cookies, router, error handling), an expected Node/Express backend practice. Source:
src/server.ts.
export const createServer = async () => {
const server: express.Application = express();
server.use(bodyParser.urlencoded({ extended: true }))
server.use(bodyParser.json())
server.use('/api-docs',swaggerUI.serve,swaggerUI.setup(swDocument))
server.use(cookieParser());
if (NODE_ENV === 'development') {
server.use(morgan('dev'));
}
server.use(APP_BASE_URL as string, mainRouter)
server.use(notFoundRouter)
server.use(errorHandler)
if (NODE_ENV === 'development') {
server.use(errorLogging);
}
return server
}
- cda-my-happy-wallet-backend: relational modeling with Prisma (strong typing, relations, enums), a key persistence-layer competency. Source:
src/database/schema.prisma.
model OperationFixe {
idOperationFixe Int @id @default(autoincrement())
titre String @db.VarChar(50)
montant Decimal @db.Decimal(10, 2)
devise String? @db.VarChar(3)
typeOperation TypeOperationFixeEnum @default(CHARGE)
User Utilisateur @relation(fields: [userId],references: [id])
userId Int
}
model Utilisateur {
id Int @id @default(autoincrement())
email String @db.VarChar(255) @unique
role Role @default(USER)
verified Boolean @default(false)
operationsFixes OperationFixe[]
}
enum TypeOperationFixeEnum { CHARGE REVENU }
enum Role { ADMIN USER }
- cda-my-happy-wallet-backend: robust authentication flow (account verification + Argon2 hashing + JWT access/refresh issuance). Source:
src/modules/user/useCases/login/login.ts.
const user = await this.userRepo.getUserByEmail(email);
if (!user) {
throw new ErrorException(ErrorCode.EmailPasswordNotValid);
}
const isAccountVerified = await this.userRepo.isUserAccountVerified(email)
if (!isAccountVerified) {
throw new ErrorException(ErrorCode.EmailPasswordNotValid);
}
const passwordMatches = await argon2.verify(user.password,password)
if (!passwordMatches) {
throw new ErrorException(ErrorCode.EmailPasswordNotValid);
}
const jwtToken = sign({ id: user.id }, ACCESS_TOKEN_SECRET as string, {expiresIn:"60s"})
const refreshToken = sign({ id: user.id }, REFRESH_TOKEN_SECRET as string, {expiresIn:"15min"})
- cda-my-happy-wallet-backend: token rotation through a dedicated endpoint (user-context validation + secure cookie renewal). Source:
src/modules/auth/accessTokenRenew.ts.
jwt.verify(cookies.refresh_token, REFRESH_TOKEN_SECRET as string, (err: any) => {
if (err) {
res.clearCookie("refresh_token");
res.clearCookie("id_user");
return next(new ErrorException(ErrorCode.Unauthorized));
}
const accessToken = jwt.sign({ id: user.id }, ACCESS_TOKEN_SECRET as string, {
expiresIn: "5min",
});
const refreshToken = jwt.sign({ id: user.id }, REFRESH_TOKEN_SECRET as string, {
expiresIn: "20min",
});
res.cookie("id_user", user.id, { httpOnly: true, secure: true, maxAge: 900000 });
res.cookie("refresh_token", refreshToken, { httpOnly: true, secure: true, maxAge: 900000 });
return res.status(200).json({ success: true, payload: { user: data, accessToken } });
});
- cda-my-happy-wallet-backend: business computation encapsulated at repository/use-case level to keep financial aggregates consistent (disposable income). Source:
src/modules/operationsFixes/operationFixeRepo.ts.
public async updateRaV(userId:string, idRaV:number){
const allRevenus = await this.getAllRevenus(userId,"REVENU")
const allCharges = await this.getAllCharges(userId,"CHARGE")
const totalCharges = allCharges.data.reduce(
(accumulator:any, current:any) => accumulator + parseFloat(current.montant), 0
);
const totalRevenus = allRevenus.data.reduce(
(accumulator:any, current:any) => accumulator + parseFloat(current.montant), 0
);
const rav = totalRevenus - totalCharges;
await RaVEntity.updateMany({
where: { idRaV: idRaV, userId: parseInt(userId) },
data: { montantRaV: rav, montantTotalDepense: totalCharges, montantTotalEntree: totalRevenus },
})
}
- cda-my-happy-wallet-backend: backend DevOps industrialization with GitLab CI + PM2 deployment (pre-prod/prod). Source:
.gitlab-ci.yml.
build:
stage: deploy_pre_prod
rules:
- if: '$CI_COMMIT_BRANCH == "develop"'
when: always
script:
- node --version
- pm2 deploy ecosystem.config.js development setup 2>&1 || true
- pm2 deploy ecosystem.config.js development
deploy:
stage: deploy_prod
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: always
script:
- pm2 deploy ecosystem.config.js production setup 2>&1 || true
- pm2 deploy ecosystem.config.js production
- cda-my-happy-wallet-frontend: SPA structure with public/private routes, protected navigation, and explicit fallback route. Source:
src/js/App.jsx.
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/forgot-password" element={<ForgotPassword />} />
<Route path="/new-password" element={<NewPassword />} />
<Route path="home" element={<RequireAuth><Sidebar /><Home /></RequireAuth>} />
<Route path="home/operations-fixes" element={<RequireAuth><Sidebar /><OperationsFixes/></RequireAuth>} />
<Route path="/" element={<Navigate replace to="/home" />} />
<Route path="/calendrier" element={<RequireAuth><Sidebar /><Calendrier /></RequireAuth>} />
<Route path="*" element={<NotFound />}></Route>
</Routes>
- cda-my-happy-wallet-frontend: Axios interceptor for automatic expired-token refresh, a common secure-frontend pattern. Source:
src/utils/axiosHelper.js.
if (user && user?.payload.accessToken) {
let accessToken = user.payload.accessToken;
req.headers.Authorization = `Bearer ${accessToken}`;
const decodedToken = jwt_decode(accessToken);
const isExpired = decodedToken.exp * 1000 < currentDate.getTime();
if (!isExpired) return req;
let body = { grant_type: "refresh_token", email: user.payload.user.email };
await store.dispatch(newRefreshToken({ body, accessToken }));
let newAccessToken = store?.getState()?.auth?.user.payload.accessToken;
req.headers.Authorization = `Bearer ${newAccessToken}`;
req.withCredentials = true;
return req;
}
- cda-my-happy-wallet-frontend: asynchronous state management with Redux Toolkit (thunks + lifecycle reducers), a standard production React pattern. Source:
src/js/slices/operationsFixes/operationsFixesSlice.js.
export const revenusApi = createAsyncThunk('operationsFixes/revenus', async (thunkAPI) => {
try {
const response = await operationsFixesService.getAllRevenus()
return response.data
} catch (error) {
const ErrorObjet = await handleExceptionPayload(error)
return thunkAPI.rejectWithValue(ErrorObjet.message)
}
})
export const operationsFixesSlice = createSlice({
name: "operationsFixes",
initialState,
extraReducers:(builder)=>{
builder
.addCase(revenusApi.pending, (state) => { state.isLoading = true })
.addCase(revenusApi.fulfilled, (state, action) => {
state.isLoading = false
state.revenus.isSuccess = true
state.revenus.data = action.payload.data
})
}
});
🏗️ Infrastructure and deployment
- Backend: GitLab CI pipeline with PM2 deployment (
developandmainbranches) and versionedpost-deployscripts. - Frontend: GitLab CI pipeline with Vite build and
dist/artifact synchronization to server viarsync. - Multi-environment deployment observed in configuration (
development/production) and CI variable management.
🧭 Organization / methodology
- Product-oriented workflow: vision, story mapping, user stories, UML/Merise modeling, then technical execution.
- Clear separation of concerns in code: routes/controllers/use cases/repos on backend; services/slices/components on frontend.
- Technical documentation embedded in the project (detailed report + code annexes + Swagger).
📈 Results
- Global result: 212 consolidated commits across 3 repositories, covering the full flow from design to development and deployment.
Project cda-my-happy-wallet-backend
- Commit count: 104
- Contributor count: 2
- PR/issues: not consolidated in this local scope
- Period: 2022-01-12 -> 2026-02-14
Project cda-my-happy-wallet-frontend
- Commit count: 53
- Contributor count: 2
- PR/issues: not consolidated in this local scope
- Period: 2022-01-12 -> 2026-02-14
Project Projet-00-chef-oeuvre-CDA
- Commit count: 55
- Contributor count: 2
- PR/issues: not consolidated in this local scope
- Period: 2021-08-01 -> 2022-04-25
🔧 Technical environment
- Backend: TypeScript, Node.js, Express, Prisma, MySQL, Joi, JSON Web Token, Argon2, Swagger UI, PM2.
- Frontend: React, React Router, Redux Toolkit, Axios, Formik, Yup, Styled Components, Vite.
- DevOps: GitLab CI/CD, PM2 deploy, rsync, CI runners, environment management.
- Design/documentation: LaTeX (minted), Figma, UML, Merise, auxiliary Python scripts for technical publishing.
Tech Stack
Backend
Express
Node.js
DevOps
GitLab CI/CD
Bases de donnees (SGBD & SQL)
MySQL
Design Patterns & Architecture
Prisma
Frontend
React
TypeScript