myhappywallet

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

🧩 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

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
}
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 }
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"})
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 } });
});
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 },
  })
}
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
<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>
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;
}
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 (develop and main branches) and versioned post-deploy scripts.
  • Frontend: GitLab CI pipeline with Vite build and dist/ artifact synchronization to server via rsync.
  • 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