From 1aaff5edc3bcedb9e8c3f4f085a06d164521a340 Mon Sep 17 00:00:00 2001 From: TTalex Date: Fri, 20 Sep 2024 18:54:28 +0200 Subject: [PATCH 01/10] Allow Maas Backend Role read rights on get /v1/subscriptions --- api/src/controllers/subscription.controller.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/api/src/controllers/subscription.controller.ts b/api/src/controllers/subscription.controller.ts index ffefa70..0a39851 100644 --- a/api/src/controllers/subscription.controller.ts +++ b/api/src/controllers/subscription.controller.ts @@ -62,6 +62,7 @@ import { StatusCode, SECURITY_SPEC_KC_PASSWORD, SECURITY_SPEC_KC_CREDENTIALS_KC_PASSWORD, + SECURITY_SPEC_JWT_KC_PASSWORD_KC_CREDENTIALS, INCENTIVE_TYPE, Roles, SUBSCRIPTION_STATUS, @@ -448,11 +449,11 @@ export class SubscriptionController { * @param citizenId the citizen id * @returns subscription list */ - @authorize({allowedRoles: [Roles.MAAS, Roles.MANAGERS, Roles.CITIZENS]}) + @authorize({allowedRoles: [Roles.MAAS, Roles.MANAGERS, Roles.CITIZENS, Roles.MAAS_BACKEND]}) @get('/v1/subscriptions', { 'x-controller-name': 'Subscriptions', summary: 'Retourne les souscriptions', - security: SECURITY_SPEC_JWT_KC_PASSWORD, + security: SECURITY_SPEC_JWT_KC_PASSWORD_KC_CREDENTIALS, responses: { [StatusCode.Success]: { description: 'La liste des souscriptions', From 8462203172749e6f012f6079a94880128ede34cd Mon Sep 17 00:00:00 2001 From: TTalex Date: Mon, 7 Oct 2024 18:46:00 +0200 Subject: [PATCH 02/10] Afapast first commit: A voucher system --- afapast/.gitignore | 68 ++++++++ afapast/Dockerfile | 34 ++++ afapast/README.md | 43 +++++ afapast/data/db.sqlite | Bin 0 -> 12288 bytes afapast/package.json | 79 +++++++++ afapast/public/index.html | 88 ++++++++++ afapast/src/__tests__/README.md | 3 + .../acceptance/home-page.acceptance.ts | 31 ++++ .../src/__tests__/acceptance/test-helper.ts | 32 ++++ afapast/src/application.ts | 76 +++++++++ afapast/src/config/mailConfig.ts | 29 ++++ afapast/src/controllers/README.md | 9 ++ afapast/src/controllers/index.ts | 2 + .../tracked-incentives.controller.ts | 152 ++++++++++++++++++ afapast/src/controllers/voucher.controller.ts | 151 +++++++++++++++++ afapast/src/cronjob/handling.cronjob.ts | 113 +++++++++++++ afapast/src/cronjob/index.ts | 1 + afapast/src/datasources/README.md | 3 + afapast/src/datasources/db.datasource.ts | 26 +++ afapast/src/datasources/index.ts | 1 + afapast/src/index.ts | 38 +++++ afapast/src/migrate.ts | 20 +++ afapast/src/models/README.md | 3 + afapast/src/models/index.ts | 2 + .../src/models/tracked-incentives.model.ts | 44 +++++ afapast/src/models/voucher.model.ts | 60 +++++++ afapast/src/openapi-spec.ts | 23 +++ afapast/src/repositories/README.md | 3 + afapast/src/repositories/index.ts | 2 + .../tracked-incentives.repository.ts | 16 ++ .../src/repositories/voucher.repository.ts | 16 ++ afapast/src/sequence.ts | 3 + .../src/services/authentication.service.ts | 31 ++++ afapast/src/services/index.ts | 2 + afapast/src/services/mail.service.ts | 41 +++++ afapast/src/services/mob.service.ts | 79 +++++++++ afapast/src/strategies/api-key.strategy.ts | 21 +++ afapast/templates/commons/button.ejs | 59 +++++++ afapast/templates/commons/footer.ejs | 22 +++ afapast/templates/commons/header.ejs | 10 ++ afapast/templates/commons/utils.ejs | 11 ++ afapast/templates/css/style.ejs | 143 ++++++++++++++++ afapast/templates/voucher-airweb.ejs | 22 +++ afapast/tsconfig.json | 9 ++ 44 files changed, 1621 insertions(+) create mode 100644 afapast/.gitignore create mode 100644 afapast/Dockerfile create mode 100644 afapast/README.md create mode 100644 afapast/data/db.sqlite create mode 100644 afapast/package.json create mode 100644 afapast/public/index.html create mode 100644 afapast/src/__tests__/README.md create mode 100644 afapast/src/__tests__/acceptance/home-page.acceptance.ts create mode 100644 afapast/src/__tests__/acceptance/test-helper.ts create mode 100644 afapast/src/application.ts create mode 100644 afapast/src/config/mailConfig.ts create mode 100644 afapast/src/controllers/README.md create mode 100644 afapast/src/controllers/index.ts create mode 100644 afapast/src/controllers/tracked-incentives.controller.ts create mode 100644 afapast/src/controllers/voucher.controller.ts create mode 100644 afapast/src/cronjob/handling.cronjob.ts create mode 100644 afapast/src/cronjob/index.ts create mode 100644 afapast/src/datasources/README.md create mode 100644 afapast/src/datasources/db.datasource.ts create mode 100644 afapast/src/datasources/index.ts create mode 100644 afapast/src/index.ts create mode 100644 afapast/src/migrate.ts create mode 100644 afapast/src/models/README.md create mode 100644 afapast/src/models/index.ts create mode 100644 afapast/src/models/tracked-incentives.model.ts create mode 100644 afapast/src/models/voucher.model.ts create mode 100644 afapast/src/openapi-spec.ts create mode 100644 afapast/src/repositories/README.md create mode 100644 afapast/src/repositories/index.ts create mode 100644 afapast/src/repositories/tracked-incentives.repository.ts create mode 100644 afapast/src/repositories/voucher.repository.ts create mode 100644 afapast/src/sequence.ts create mode 100644 afapast/src/services/authentication.service.ts create mode 100644 afapast/src/services/index.ts create mode 100644 afapast/src/services/mail.service.ts create mode 100644 afapast/src/services/mob.service.ts create mode 100644 afapast/src/strategies/api-key.strategy.ts create mode 100644 afapast/templates/commons/button.ejs create mode 100644 afapast/templates/commons/footer.ejs create mode 100644 afapast/templates/commons/header.ejs create mode 100644 afapast/templates/commons/utils.ejs create mode 100644 afapast/templates/css/style.ejs create mode 100644 afapast/templates/voucher-airweb.ejs create mode 100644 afapast/tsconfig.json diff --git a/afapast/.gitignore b/afapast/.gitignore new file mode 100644 index 0000000..33ca215 --- /dev/null +++ b/afapast/.gitignore @@ -0,0 +1,68 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.bat +.env.mcm + +# Transpiled JavaScript files from Typescript +/dist + +# Cache used by TypeScript's incremental build +*.tsbuildinfo + +.scannerwork \ No newline at end of file diff --git a/afapast/Dockerfile b/afapast/Dockerfile new file mode 100644 index 0000000..b821665 --- /dev/null +++ b/afapast/Dockerfile @@ -0,0 +1,34 @@ +# Check out https://hub.docker.com/_/node to select a new base image +FROM docker.io/library/node:20 + +# Install Python, make, g++, and other build tools required by node-gyp +RUN apt-get update && \ + apt-get install -y python3 make g++ && \ + ln -s /usr/bin/python3 /usr/bin/python + +# Set to a non-root built-in user `node` +USER node + +# Create app directory (with user `node`) +RUN mkdir -p /home/node/app + +WORKDIR /home/node/app + +# Install app dependencies + +# A wildcard is used to ensure both package.json AND package-lock.json are copied +# where available (npm@5+) +COPY --chown=node package*.json ./ + +RUN npm install + +# Bundle app source code +COPY --chown=node . . + +RUN npm run build + +# Bind to all network interfaces so that it can be mapped to the host OS +ENV HOST=0.0.0.0 PORT=3002 + +EXPOSE ${PORT} +CMD [ "node", "." ] diff --git a/afapast/README.md b/afapast/README.md new file mode 100644 index 0000000..34d08ed --- /dev/null +++ b/afapast/README.md @@ -0,0 +1,43 @@ +# Afapast + +A voucher distribution tool, reading mob subscription API and sending emails with vouchers. + +Also an API in itself to manage tracked incentives and vouchers. + +## Install dependencies + +By default, dependencies were installed when this application was generated. +Whenever dependencies in `package.json` are changed, run the following command: + +```sh +npm install +``` + + +## Run the application + +Migrate the db if it's the first time +```sh +yarn migrate +``` + +```sh +yarn start +``` + +Open http://127.0.0.1:3002 in your browser. + +## Env variables + +| Variables | Description | Mandatory | Default value if unspecified | +| ------------------------ | ------------------------------------------------------------ | ----------- | ---------------------------- | +| HOST | Service host | No | '127.0.0.1' | +| PORT | Service port | No | 3002 | +| API_KEY | Api Key authorizing queries to this project API | No, but recommended | 'apikey' | +| EMAIL_FROM | email used as a sender, only used with NODE_ENV='production' | Only in production | | +| EMAIL_HOST | smtp host, only used with NODE_ENV='production' | Only in production | | +| EMAIL_PORT | smtp port, only used with NODE_ENV='production' | Only in production | | +| MOB_TOKEN_URL | oidc token url for mob | No | 'http://localhost:9000/auth/realms/mcm/protocol/openid-connect/token' | +| MOB_CLIENT_ID | oidc client id to fetch a token | No | 'simulation-maas-backend' | +| MOB_CLIENT_SECRET | oidc client secret to fetch a token | No | '4x1zfk4p4d7ZdLPAsaWBhd5mu86n5ZWN' | +| MOB_API_SUBSCRIPTION_URL | subscription url | No | 'http://localhost:3000/v1/subscriptions' | diff --git a/afapast/data/db.sqlite b/afapast/data/db.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..3f63ff0177197c0421d46fa314cb33122ecceb39 GIT binary patch literal 12288 zcmeI#O;5rw7zglfm~ist#(~R|o+Mf1z}YC8#-Iar&P0x7tD$iWvvnM}!MF6=*vTL$ zG4YK5CSBX7P5bn>Q=cAvZ4|vuvxzj+VJFOS*d-BT%(b;<>$;TVgZ03+&Q|}ouFK9Y zJ`U}ydo*XqxAubq0SG_<0uX=z1Rwwb2tWV=|4d-fck7LI+gbLE93{$RGI~?7P9v2X zJyZEkpmrUIZYU`1UipIf&OE16u47KUK`3rSK!f{`2G71v4?(Zr4Tf|lh92J>P%aI{ zi(ON#^LQfdsY=F1Pt<0gRX82x#VF@wQ}8{$8jWQdC(3S923w1cQ*Shz&hq^0KC@{N zja61P?)TiQ=-)D)$)vF9{Kz#o(iAqKGVJj(xhT``Mo-iA#XKHqqvvWXuygrV`8KzY zeiR5m00Izz00bZa0SG_<0uX=z1ol_}@BcmixwskxAOHafKmY;|fB*y_009U + + + + afapast + + + + + + + + + + +
+

afapast

+

Version 1.0.0

+ +

OpenAPI spec: /openapi.json

+

API Explorer: /explorer

+
+ + + + + diff --git a/afapast/src/__tests__/README.md b/afapast/src/__tests__/README.md new file mode 100644 index 0000000..a88f8a5 --- /dev/null +++ b/afapast/src/__tests__/README.md @@ -0,0 +1,3 @@ +# Tests + +Please place your tests in this folder. diff --git a/afapast/src/__tests__/acceptance/home-page.acceptance.ts b/afapast/src/__tests__/acceptance/home-page.acceptance.ts new file mode 100644 index 0000000..6758585 --- /dev/null +++ b/afapast/src/__tests__/acceptance/home-page.acceptance.ts @@ -0,0 +1,31 @@ +import {Client} from '@loopback/testlab'; +import {AfapastApplication} from '../..'; +import {setupApplication} from './test-helper'; + +describe('HomePage', () => { + let app: AfapastApplication; + let client: Client; + + before('setupApplication', async () => { + ({app, client} = await setupApplication()); + }); + + after(async () => { + await app.stop(); + }); + + it('exposes a default home page', async () => { + await client + .get('/') + .expect(200) + .expect('Content-Type', /text\/html/); + }); + + it('exposes self-hosted explorer', async () => { + await client + .get('/explorer/') + .expect(200) + .expect('Content-Type', /text\/html/) + .expect(/LoopBack API Explorer/); + }); +}); diff --git a/afapast/src/__tests__/acceptance/test-helper.ts b/afapast/src/__tests__/acceptance/test-helper.ts new file mode 100644 index 0000000..c824843 --- /dev/null +++ b/afapast/src/__tests__/acceptance/test-helper.ts @@ -0,0 +1,32 @@ +import {AfapastApplication} from '../..'; +import { + createRestAppClient, + givenHttpServerConfig, + Client, +} from '@loopback/testlab'; + +export async function setupApplication(): Promise<AppWithClient> { + const restConfig = givenHttpServerConfig({ + // Customize the server configuration here. + // Empty values (undefined, '') will be ignored by the helper. + // + // host: process.env.HOST, + // port: +process.env.PORT, + }); + + const app = new AfapastApplication({ + rest: restConfig, + }); + + await app.boot(); + await app.start(); + + const client = createRestAppClient(app); + + return {app, client}; +} + +export interface AppWithClient { + app: AfapastApplication; + client: Client; +} diff --git a/afapast/src/application.ts b/afapast/src/application.ts new file mode 100644 index 0000000..8b5a082 --- /dev/null +++ b/afapast/src/application.ts @@ -0,0 +1,76 @@ +import {BootMixin} from '@loopback/boot'; +import {ApplicationConfig, createBindingFromClass} from '@loopback/core'; +import { + RestExplorerBindings, + RestExplorerComponent, +} from '@loopback/rest-explorer'; +import {RepositoryMixin} from '@loopback/repository'; +import {mergeOpenAPISpec, RestApplication, RestBindings} from '@loopback/rest'; +import {ServiceMixin} from '@loopback/service-proxy'; +import path from 'path'; +import {MySequence} from './sequence'; +import {AuthenticationComponent, registerAuthenticationStrategy} from '@loopback/authentication'; +import { ApiKeyAuthenticationStrategy } from './strategies/api-key.strategy'; +import {CronComponent} from '@loopback/cron'; +import { + HandlingCronJob, +} from './cronjob/handling.cronjob'; + + +export {ApplicationConfig}; + +export class AfapastApplication extends BootMixin( + ServiceMixin(RepositoryMixin(RestApplication)), +) { + constructor(options: ApplicationConfig = {}) { + super(options); + + // Set up the custom sequence + this.sequence(MySequence); + + this.api( + mergeOpenAPISpec(this.getSync(RestBindings.API_SPEC), { + // info: infoObject, + components: { + securitySchemes: { + ApiKey: { + type: 'apiKey', + name: 'X-API-Key', + in: 'header', + } + }, + }, + security: [ + { + ApiKey: [], + }, + ], + }), + ); + + // Set up default home page + this.static('/', path.join(__dirname, '../public')); + + // Customize @loopback/rest-explorer configuration here + this.configure(RestExplorerBindings.COMPONENT).to({ + path: '/explorer', + }); + this.component(RestExplorerComponent); + + this.component(CronComponent); + this.add(createBindingFromClass(HandlingCronJob)); + + this.projectRoot = __dirname; + // Customize @loopback/boot Booter Conventions here + this.bootOptions = { + controllers: { + // Customize ControllerBooter Conventions here + dirs: ['controllers'], + extensions: ['.controller.js'], + nested: true, + }, + }; + this.component(AuthenticationComponent); + registerAuthenticationStrategy(this, ApiKeyAuthenticationStrategy); + } +} diff --git a/afapast/src/config/mailConfig.ts b/afapast/src/config/mailConfig.ts new file mode 100644 index 0000000..6211e09 --- /dev/null +++ b/afapast/src/config/mailConfig.ts @@ -0,0 +1,29 @@ +import nodemailer from 'nodemailer'; + +export class MailConfig { + /** + * Check env before sending the email. + */ + configMailer() { + let mailer, from; + + // TODO, what params are here ? + const production = { + host: process.env.EMAIL_HOST!, + port: parseInt(process.env.EMAIL_PORT!), + }; + + // check landscape + if (process.env.NODE_ENV === 'production') { + mailer = nodemailer.createTransport(production); + from = process.env.EMAIL_FROM; + } else { + mailer = nodemailer.createTransport({ + port: 1025, + }); + from = 'Mon Compte Mobilité <mcm.mailhog@gmail.com>';; + } + + return {mailer, from}; + } +} diff --git a/afapast/src/controllers/README.md b/afapast/src/controllers/README.md new file mode 100644 index 0000000..ad4c4cc --- /dev/null +++ b/afapast/src/controllers/README.md @@ -0,0 +1,9 @@ +# Controllers + +This directory contains source files for the controllers exported by this app. + +To add a new empty controller, type in `lb4 controller [<name>]` from the +command-line of your application's root directory. + +For more information, please visit +[Controller generator](http://loopback.io/doc/en/lb4/Controller-generator.html). diff --git a/afapast/src/controllers/index.ts b/afapast/src/controllers/index.ts new file mode 100644 index 0000000..d8d25de --- /dev/null +++ b/afapast/src/controllers/index.ts @@ -0,0 +1,2 @@ +export * from './voucher.controller'; +export * from './tracked-incentives.controller'; diff --git a/afapast/src/controllers/tracked-incentives.controller.ts b/afapast/src/controllers/tracked-incentives.controller.ts new file mode 100644 index 0000000..41e49f4 --- /dev/null +++ b/afapast/src/controllers/tracked-incentives.controller.ts @@ -0,0 +1,152 @@ +import { + Count, + CountSchema, + Filter, + FilterExcludingWhere, + repository, + Where, +} from '@loopback/repository'; +import { + post, + param, + get, + getModelSchemaRef, + patch, + put, + del, + requestBody, + response, +} from '@loopback/rest'; +import {TrackedIncentives} from '../models'; +import {TrackedIncentivesRepository} from '../repositories'; +import { authenticate } from '@loopback/authentication'; + +@authenticate('api-key') +export class TrackedIncentivesController { + constructor( + @repository(TrackedIncentivesRepository) + public trackedIncentivesRepository : TrackedIncentivesRepository, + ) {} + + @post('/tracked-incentives') + @response(200, { + description: 'TrackedIncentives model instance', + content: {'application/json': {schema: getModelSchemaRef(TrackedIncentives)}}, + }) + async create( + @requestBody({ + content: { + 'application/json': { + schema: getModelSchemaRef(TrackedIncentives, { + title: 'NewTrackedIncentives', + exclude: ['id'], + }), + }, + }, + }) + trackedIncentives: Omit<TrackedIncentives, 'id'>, + ): Promise<TrackedIncentives> { + return this.trackedIncentivesRepository.create(trackedIncentives); + } + + @get('/tracked-incentives/count') + @response(200, { + description: 'TrackedIncentives model count', + content: {'application/json': {schema: CountSchema}}, + }) + async count( + @param.where(TrackedIncentives) where?: Where<TrackedIncentives>, + ): Promise<Count> { + return this.trackedIncentivesRepository.count(where); + } + + @get('/tracked-incentives') + @response(200, { + description: 'Array of TrackedIncentives model instances', + content: { + 'application/json': { + schema: { + type: 'array', + items: getModelSchemaRef(TrackedIncentives, {includeRelations: true}), + }, + }, + }, + }) + async find( + @param.filter(TrackedIncentives) filter?: Filter<TrackedIncentives>, + ): Promise<TrackedIncentives[]> { + return this.trackedIncentivesRepository.find(filter); + } + + @patch('/tracked-incentives') + @response(200, { + description: 'TrackedIncentives PATCH success count', + content: {'application/json': {schema: CountSchema}}, + }) + async updateAll( + @requestBody({ + content: { + 'application/json': { + schema: getModelSchemaRef(TrackedIncentives, {partial: true}), + }, + }, + }) + trackedIncentives: TrackedIncentives, + @param.where(TrackedIncentives) where?: Where<TrackedIncentives>, + ): Promise<Count> { + return this.trackedIncentivesRepository.updateAll(trackedIncentives, where); + } + + @get('/tracked-incentives/{id}') + @response(200, { + description: 'TrackedIncentives model instance', + content: { + 'application/json': { + schema: getModelSchemaRef(TrackedIncentives, {includeRelations: true}), + }, + }, + }) + async findById( + @param.path.number('id') id: number, + @param.filter(TrackedIncentives, {exclude: 'where'}) filter?: FilterExcludingWhere<TrackedIncentives> + ): Promise<TrackedIncentives> { + return this.trackedIncentivesRepository.findById(id, filter); + } + + @patch('/tracked-incentives/{id}') + @response(204, { + description: 'TrackedIncentives PATCH success', + }) + async updateById( + @param.path.number('id') id: number, + @requestBody({ + content: { + 'application/json': { + schema: getModelSchemaRef(TrackedIncentives, {partial: true}), + }, + }, + }) + trackedIncentives: TrackedIncentives, + ): Promise<void> { + await this.trackedIncentivesRepository.updateById(id, trackedIncentives); + } + + @put('/tracked-incentives/{id}') + @response(204, { + description: 'TrackedIncentives PUT success', + }) + async replaceById( + @param.path.number('id') id: number, + @requestBody() trackedIncentives: TrackedIncentives, + ): Promise<void> { + await this.trackedIncentivesRepository.replaceById(id, trackedIncentives); + } + + @del('/tracked-incentives/{id}') + @response(204, { + description: 'TrackedIncentives DELETE success', + }) + async deleteById(@param.path.number('id') id: number): Promise<void> { + await this.trackedIncentivesRepository.deleteById(id); + } +} diff --git a/afapast/src/controllers/voucher.controller.ts b/afapast/src/controllers/voucher.controller.ts new file mode 100644 index 0000000..c42ffb9 --- /dev/null +++ b/afapast/src/controllers/voucher.controller.ts @@ -0,0 +1,151 @@ +import { + Count, + CountSchema, + Filter, + FilterExcludingWhere, + repository, + Where, +} from '@loopback/repository'; +import { + post, + param, + get, + getModelSchemaRef, + patch, + put, + del, + requestBody, + response, +} from '@loopback/rest'; +import {Voucher} from '../models'; +import {VoucherRepository} from '../repositories'; +import { authenticate } from '@loopback/authentication'; + +@authenticate('api-key') +export class VoucherController { + constructor( + @repository(VoucherRepository) + public voucherRepository : VoucherRepository, + ) {} + + @post('/vouchers') + @response(200, { + description: 'Voucher model instance', + content: {'application/json': {schema: getModelSchemaRef(Voucher)}}, + }) + async create( + @requestBody({ + content: { + 'application/json': { + schema: getModelSchemaRef(Voucher, { + title: 'NewVoucher', + exclude: ['id'], + }), + }, + }, + }) + voucher: Omit<Voucher, 'id'>, + ): Promise<Voucher> { + return this.voucherRepository.create(voucher); + } + + @get('/vouchers/count') + @response(200, { + description: 'Voucher model count', + content: {'application/json': {schema: CountSchema}}, + }) + async count( + @param.where(Voucher) where?: Where<Voucher>, + ): Promise<Count> { + return this.voucherRepository.count(where); + } + @get('/vouchers') + @response(200, { + description: 'Array of Voucher model instances', + content: { + 'application/json': { + schema: { + type: 'array', + items: getModelSchemaRef(Voucher, {includeRelations: true}), + }, + }, + }, + }) + async find( + @param.filter(Voucher) filter?: Filter<Voucher>, + ): Promise<Voucher[]> { + return this.voucherRepository.find(filter); + } + + @patch('/vouchers') + @response(200, { + description: 'Voucher PATCH success count', + content: {'application/json': {schema: CountSchema}}, + }) + async updateAll( + @requestBody({ + content: { + 'application/json': { + schema: getModelSchemaRef(Voucher, {partial: true}), + }, + }, + }) + voucher: Voucher, + @param.where(Voucher) where?: Where<Voucher>, + ): Promise<Count> { + return this.voucherRepository.updateAll(voucher, where); + } + + @get('/vouchers/{id}') + @response(200, { + description: 'Voucher model instance', + content: { + 'application/json': { + schema: getModelSchemaRef(Voucher, {includeRelations: true}), + }, + }, + }) + async findById( + @param.path.number('id') id: number, + @param.filter(Voucher, {exclude: 'where'}) filter?: FilterExcludingWhere<Voucher> + ): Promise<Voucher> { + return this.voucherRepository.findById(id, filter); + } + + @patch('/vouchers/{id}') + @response(204, { + description: 'Voucher PATCH success', + }) + async updateById( + @param.path.number('id') id: number, + @requestBody({ + content: { + 'application/json': { + schema: getModelSchemaRef(Voucher, {partial: true}), + }, + }, + }) + voucher: Voucher, + ): Promise<void> { + await this.voucherRepository.updateById(id, voucher); + } + + @put('/vouchers/{id}') + @response(204, { + description: 'Voucher PUT success', + }) + async replaceById( + @param.path.number('id') id: number, + @requestBody() voucher: Voucher, + ): Promise<void> { + await this.voucherRepository.replaceById(id, voucher); + } + + @del('/vouchers/{id}') + @response(204, { + description: 'Voucher DELETE success', + }) + async deleteById(@param.path.number('id') id: number): Promise<void> { + await this.voucherRepository.deleteById(id); + } +} diff --git a/afapast/src/cronjob/handling.cronjob.ts b/afapast/src/cronjob/handling.cronjob.ts new file mode 100644 index 0000000..bd51d19 --- /dev/null +++ b/afapast/src/cronjob/handling.cronjob.ts @@ -0,0 +1,113 @@ +import {service} from '@loopback/core'; +import {CronJob, cronJob} from '@loopback/cron'; + +import {MobService} from '../services'; +import { repository } from '@loopback/repository'; +import { TrackedIncentivesRepository, VoucherRepository } from '../repositories'; +import { VOUCHER_STATUS } from '../models/voucher.model'; +import { MailService } from '../services/mail.service'; + +function capitalize(str: string) { + return str.charAt(0).toUpperCase() + str.slice(1); +} + +@cronJob() +export class HandlingCronJob extends CronJob { + // Cron's type + + constructor( + @service(MobService) + public mobService: MobService, + @service(MailService) + public mailService: MailService, + @repository(TrackedIncentivesRepository) + public trackedIncentivesRepository : TrackedIncentivesRepository, + @repository(VoucherRepository) + public voucherRepository : VoucherRepository, + ) { + super({ + name: 'subscription-job', + onTick: async () => { + await this.performJob(); + }, + cronTime: '* * * * *', // every minute + start: false, + }); + } + + /** + * Perform cron job + */ + private async performJob(): Promise<void> { + console.debug("doing the job !") + const trackedIncentives = await this.trackedIncentivesRepository.find() + for (let i = 0; i < trackedIncentives.length; i++) { + const trackedIncentive = trackedIncentives[i]; + const subscriptions = await this.mobService.subscriptionsFind(trackedIncentive.incentiveId, "VALIDEE") + console.debug(subscriptions.length + " subs loaded from api") + + let nbSubsHandled = 0 + for (let j = 0; j < subscriptions.length; j++) { + const subscription = subscriptions[j]; + + if (process.env.MINIMAL_SUBSCRIPTION_START_DATE && subscription.updatedAt < process.env.MINIMAL_SUBSCRIPTION_START_DATE) { + // Sub is too old, ignore it + continue + } + // Check if sub was already handled + const voucherGiven = await this.voucherRepository.find({ + where: { + subscriptionId: subscription.id + } + }) + if (voucherGiven.length > 0) { + // skip subscription already handled + continue + } + console.debug("Found one sub to be handled : " + subscription.id) + // Get a voucher + const vouchers = await this.voucherRepository.find({ + where: { + status: VOUCHER_STATUS.UNUSED + }, + limit: 1 + }) + if (!vouchers || vouchers.length === 0) { + // Uh oh, not enough vouchers :( + console.error("Not enough vouchers") + return + } + const voucher = vouchers[0] + console.debug("Got an unused voucher : " + voucher.id) + + // MAIL ! + await this.mailService.sendMailAsHtml(subscription.email, "Votre bon de réduction Airweb", "voucher-airweb", { + username: capitalize(subscription.firstName), + voucher: voucher.value + }) + console.debug("mail sent to " + subscription.email) + + // If mail ok, mark the voucher as used + await this.voucherRepository.updateById(voucher.id, { + status: VOUCHER_STATUS.USED, + subscriptionId: subscription.id, + citizenId: subscription.citizenId, + incentiveId: trackedIncentive.incentiveId, + }) + + console.log("voucher updated") + + nbSubsHandled++ + } + + await this.trackedIncentivesRepository.updateById(trackedIncentive.id, { + lastNbSubs: subscriptions.length, + nbSubsHandled: (trackedIncentive.nbSubsHandled || 0) + nbSubsHandled, + lastReadTime: new Date().toISOString() + }) + console.debug("Incentive updated, added " + nbSubsHandled + " subs handled") + } + + } +} + diff --git a/afapast/src/cronjob/index.ts b/afapast/src/cronjob/index.ts new file mode 100644 index 0000000..72f4971 --- /dev/null +++ b/afapast/src/cronjob/index.ts @@ -0,0 +1 @@ +export * from './handling.cronjob'; \ No newline at end of file diff --git a/afapast/src/datasources/README.md b/afapast/src/datasources/README.md new file mode 100644 index 0000000..57ae382 --- /dev/null +++ b/afapast/src/datasources/README.md @@ -0,0 +1,3 @@ +# Datasources + +This directory contains config for datasources used by this app. diff --git a/afapast/src/datasources/db.datasource.ts b/afapast/src/datasources/db.datasource.ts new file mode 100644 index 0000000..e8b2e45 --- /dev/null +++ b/afapast/src/datasources/db.datasource.ts @@ -0,0 +1,26 @@ +import {inject, lifeCycleObserver, LifeCycleObserver} from '@loopback/core'; +import {juggler} from '@loopback/repository'; + +const config = { + name: 'db', + connector: 'loopback-connector-sqlite3', + file: './data/db.sqlite' +}; + +// Observe application's life cycle to disconnect the datasource when +// application is stopped. This allows the application to be shut down +// gracefully. The `stop()` method is inherited from `juggler.DataSource`. +// Learn more at https://loopback.io/doc/en/lb4/Life-cycle.html +@lifeCycleObserver('datasource') +export class DbDataSource extends juggler.DataSource + implements LifeCycleObserver { + static dataSourceName = 'db'; + static readonly defaultConfig = config; + + constructor( + @inject('datasources.config.db', {optional: true}) + dsConfig: object = config, + ) { + super(dsConfig); + } +} diff --git a/afapast/src/datasources/index.ts b/afapast/src/datasources/index.ts new file mode 100644 index 0000000..5a9914c --- /dev/null +++ b/afapast/src/datasources/index.ts @@ -0,0 +1 @@ +export * from './db.datasource'; diff --git a/afapast/src/index.ts b/afapast/src/index.ts new file mode 100644 index 0000000..c67cb94 --- /dev/null +++ b/afapast/src/index.ts @@ -0,0 +1,38 @@ +import {ApplicationConfig, AfapastApplication} from './application'; + +export * from './application'; + +export async function main(options: ApplicationConfig = {}) { + const app = new AfapastApplication(options); + await app.boot(); + await app.start(); + + const url = app.restServer.url; + console.log(`Server is running at ${url}`); + + return app; +} + +if (require.main === module) { + // Run the application + const config = { + rest: { + port: +(process.env.PORT ?? 3002), + host: process.env.HOST || '127.0.0.1', + // The `gracePeriodForClose` provides a graceful close for http/https + // servers with keep-alive clients. The default value is `Infinity` + // (don't force-close). If you want to immediately destroy all sockets + // upon stop, set its value to `0`. + // See https://www.npmjs.com/package/stoppable + gracePeriodForClose: 5000, // 5 seconds + openApiSpec: { + // useful when used with OpenAPI-to-GraphQL to locate your application + setServersFromRequest: true, + }, + }, + }; + main(config).catch(err => { + console.error('Cannot start the application.', err); + process.exit(1); + }); +} diff --git a/afapast/src/migrate.ts b/afapast/src/migrate.ts new file mode 100644 index 0000000..fa361e2 --- /dev/null +++ b/afapast/src/migrate.ts @@ -0,0 +1,20 @@ +import {AfapastApplication} from './application'; + +export async function migrate(args: string[]) { + const existingSchema = args.includes('--rebuild') ? 'drop' : 'alter'; + console.log('Migrating schemas (%s existing schema)', existingSchema); + + const app = new AfapastApplication(); + await app.boot(); + await app.migrateSchema({existingSchema}); + + // Connectors usually keep a pool of opened connections, + // this keeps the process running even after all work is done. + // We need to exit explicitly. + process.exit(0); +} + +migrate(process.argv).catch(err => { + console.error('Cannot migrate database schema', err); + process.exit(1); +}); diff --git a/afapast/src/models/README.md b/afapast/src/models/README.md new file mode 100644 index 0000000..f5ea972 --- /dev/null +++ b/afapast/src/models/README.md @@ -0,0 +1,3 @@ +# Models + +This directory contains code for models provided by this app. diff --git a/afapast/src/models/index.ts b/afapast/src/models/index.ts new file mode 100644 index 0000000..7b697cd --- /dev/null +++ b/afapast/src/models/index.ts @@ -0,0 +1,2 @@ +export * from './voucher.model'; +export * from './tracked-incentives.model'; diff --git a/afapast/src/models/tracked-incentives.model.ts b/afapast/src/models/tracked-incentives.model.ts new file mode 100644 index 0000000..e82d792 --- /dev/null +++ b/afapast/src/models/tracked-incentives.model.ts @@ -0,0 +1,44 @@ +import {Entity, model, property} from '@loopback/repository'; + +@model() +export class TrackedIncentives extends Entity { + @property({ + type: 'number', + id: true, + generated: true, + }) + id?: number; + + @property({ + type: 'string', + required: true, + }) + incentiveId: string; + + @property({ + type: 'date', + }) + lastReadTime?: string; + + @property({ + type: 'number', + default: 0, + }) + lastNbSubs?: number; + + @property({ + type: 'number', + default: 0, + }) + nbSubsHandled?: number; + + constructor(data?: Partial<TrackedIncentives>) { + super(data); + } +} + +export interface TrackedIncentivesRelations { + // describe navigational properties here +} + +export type TrackedIncentivesWithRelations = TrackedIncentives & TrackedIncentivesRelations; diff --git a/afapast/src/models/voucher.model.ts b/afapast/src/models/voucher.model.ts new file mode 100644 index 0000000..5baa4dd --- /dev/null +++ b/afapast/src/models/voucher.model.ts @@ -0,0 +1,60 @@ +import {Entity, model, property} from '@loopback/repository'; +export enum VOUCHER_STATUS { + UNUSED = "UNUSED", + USED = "USED", + REVOKED = "REVOKED" +} + +@model() +export class Voucher extends Entity { + @property({ + type: 'number', + id: true, + generated: true, + }) + id: number; + + @property({ + type: 'string', + required: true, + }) + value: string; + + @property({ + type: 'string', + default: VOUCHER_STATUS.UNUSED, + jsonSchema: { + enum: Object.values(VOUCHER_STATUS), + }, + }) + status: VOUCHER_STATUS; + + @property({ + type: 'string', + default: '' + }) + subscriptionId?: string; + + @property({ + type: 'string', + default: '' + }) + citizenId?: string; + + @property({ + type: 'string', + default: '' + }) + incentiveId?: string; + + + constructor(data?: Partial<Voucher>) { + super(data); + } +} + +export interface VoucherRelations { + // describe navigational properties here +} + +export type VoucherWithRelations = Voucher & VoucherRelations; diff --git a/afapast/src/openapi-spec.ts b/afapast/src/openapi-spec.ts new file mode 100644 index 0000000..2319bf2 --- /dev/null +++ b/afapast/src/openapi-spec.ts @@ -0,0 +1,23 @@ +import {ApplicationConfig} from '@loopback/core'; +import {AfapastApplication} from './application'; + +/** + * Export the OpenAPI spec from the application + */ +async function exportOpenApiSpec(): Promise<void> { + const config: ApplicationConfig = { + rest: { + port: +(process.env.PORT ?? 3000), + host: process.env.HOST ?? 'localhost', + }, + }; + const outFile = process.argv[2] ?? ''; + const app = new AfapastApplication(config); + await app.boot(); + await app.exportOpenApiSpec(outFile); +} + +exportOpenApiSpec().catch(err => { + console.error('Fail to export OpenAPI spec from the application.', err); + process.exit(1); +}); diff --git a/afapast/src/repositories/README.md b/afapast/src/repositories/README.md new file mode 100644 index 0000000..08638a7 --- /dev/null +++ b/afapast/src/repositories/README.md @@ -0,0 +1,3 @@ +# Repositories + +This directory contains code for repositories provided by this app. diff --git a/afapast/src/repositories/index.ts b/afapast/src/repositories/index.ts new file mode 100644 index 0000000..2a1cb4d --- /dev/null +++ b/afapast/src/repositories/index.ts @@ -0,0 +1,2 @@ +export * from './voucher.repository'; +export * from './tracked-incentives.repository'; diff --git a/afapast/src/repositories/tracked-incentives.repository.ts b/afapast/src/repositories/tracked-incentives.repository.ts new file mode 100644 index 0000000..fcc42ec --- /dev/null +++ b/afapast/src/repositories/tracked-incentives.repository.ts @@ -0,0 +1,16 @@ +import {inject} from '@loopback/core'; +import {DefaultCrudRepository} from '@loopback/repository'; +import {DbDataSource} from '../datasources'; +import {TrackedIncentives, TrackedIncentivesRelations} from '../models'; + +export class TrackedIncentivesRepository extends DefaultCrudRepository< + TrackedIncentives, + typeof TrackedIncentives.prototype.id, + TrackedIncentivesRelations +> { + constructor( + @inject('datasources.db') dataSource: DbDataSource, + ) { + super(TrackedIncentives, dataSource); + } +} diff --git a/afapast/src/repositories/voucher.repository.ts b/afapast/src/repositories/voucher.repository.ts new file mode 100644 index 0000000..d84754a --- /dev/null +++ b/afapast/src/repositories/voucher.repository.ts @@ -0,0 +1,16 @@ +import {inject} from '@loopback/core'; +import {DefaultCrudRepository} from '@loopback/repository'; +import {DbDataSource} from '../datasources'; +import {Voucher, VoucherRelations} from '../models'; + +export class VoucherRepository extends DefaultCrudRepository< + Voucher, + typeof Voucher.prototype.id, + VoucherRelations +> { + constructor( + @inject('datasources.db') dataSource: DbDataSource, + ) { + super(Voucher, dataSource); + } +} diff --git a/afapast/src/sequence.ts b/afapast/src/sequence.ts new file mode 100644 index 0000000..2fe7751 --- /dev/null +++ b/afapast/src/sequence.ts @@ -0,0 +1,3 @@ +import {MiddlewareSequence} from '@loopback/rest'; + +export class MySequence extends MiddlewareSequence {} diff --git a/afapast/src/services/authentication.service.ts b/afapast/src/services/authentication.service.ts new file mode 100644 index 0000000..e23d8c7 --- /dev/null +++ b/afapast/src/services/authentication.service.ts @@ -0,0 +1,31 @@ +import {injectable, BindingScope} from '@loopback/core'; +import { HttpErrors } from '@loopback/rest'; +import {Request} from 'express'; + +const API_KEY = process.env.API_KEY ?? 'apikey'; + +@injectable({scope: BindingScope.TRANSIENT}) +export class AuthenticationService { + constructor() {} + + + /** + * Extract apiKey from Bearer Token + * @param request Request + * @returns string + */ + extractApiKey(request: Request): string { + const apiKeyHeaderValue = String(request.headers?.['x-api-key']); + + if (!apiKeyHeaderValue || apiKeyHeaderValue === 'undefined') { + throw new HttpErrors.Unauthorized(`Header is not of type 'X-API-Key'`); + } + + if (apiKeyHeaderValue !== API_KEY) { + throw new HttpErrors.Unauthorized(`Wrong Api Key'`); + } + + return apiKeyHeaderValue; + } + +} diff --git a/afapast/src/services/index.ts b/afapast/src/services/index.ts new file mode 100644 index 0000000..a96098b --- /dev/null +++ b/afapast/src/services/index.ts @@ -0,0 +1,2 @@ +export * from './authentication.service'; +export * from './mob.service'; diff --git a/afapast/src/services/mail.service.ts b/afapast/src/services/mail.service.ts new file mode 100644 index 0000000..8da6a6b --- /dev/null +++ b/afapast/src/services/mail.service.ts @@ -0,0 +1,41 @@ +import {injectable, BindingScope} from '@loopback/core'; +import {MailConfig} from '../config/mailConfig'; +import ejs from 'ejs'; + +export const generateTemplateAsHtml = async ( + templateName: string, + data?: Object | undefined, + ): Promise<string> => { + return await ejs.renderFile( + `./templates/${templateName}.ejs`, + data ?? {}, + ); + }; + +@injectable({scope: BindingScope.TRANSIENT}) +export class MailService { + private mailConfig: MailConfig; + + constructor() { + this.mailConfig = new MailConfig(); + } + + /** + * Sending mail with html content + * + * @param to + * @param subject + * @param templateName ejs template to be used + * @param data dict with params used in template + */ + async sendMailAsHtml(to: string, subject: string, templateName: string, data?: Object): Promise<any> { + const html = await generateTemplateAsHtml(templateName, data); + const mailerInfos = this.mailConfig.configMailer(); + await mailerInfos.mailer.sendMail({ + from: mailerInfos.from, + to: to, + subject: subject, + html: html, + }); + } +} diff --git a/afapast/src/services/mob.service.ts b/afapast/src/services/mob.service.ts new file mode 100644 index 0000000..c2c25bc --- /dev/null +++ b/afapast/src/services/mob.service.ts @@ -0,0 +1,79 @@ +import axios from 'axios'; +import {injectable, BindingScope} from '@loopback/core'; + +enum SUBSCRIPTION_STATUS { + ERROR = 'ERREUR', + TO_PROCESS = 'A_TRAITER', + VALIDATED = 'VALIDEE', + REJECTED = 'REJETEE', + DRAFT = 'BROUILLON', +} +type Subscription = { + id: string; + incentiveId: string; + funderName: string; + incentiveType: string; + incentiveTitle: string; + incentiveTransportList: string[]; + citizenId: string; + lastName: string; + firstName: string; + email: string; + city?: string; + postcode?: string; + birthdate: string; + communityId?: string; + consent: boolean; + status: SUBSCRIPTION_STATUS; + createdAt: string; + updatedAt: string; + funderId: string; + specificFields?: {[prop: string]: any}; + isCitizenDeleted: boolean; + enterpriseEmail?: string; +} + +// TODO: move params & URLs to env +async function getAccessToken(): Promise<string> { + const tokenUrl = process.env.MOB_TOKEN_URL ?? 'http://localhost:9000/auth/realms/mcm/protocol/openid-connect/token' + const client_id = process.env.MOB_CLIENT_ID ?? 'simulation-maas-backend' + const client_secret = process.env.MOB_CLIENT_SECRET ?? '4x1zfk4p4d7ZdLPAsaWBhd5mu86n5ZWN' + const data = new URLSearchParams({ + grant_type: 'client_credentials', + client_id: client_id, + client_secret: client_secret, + }); + + try { + const response = await axios.post(tokenUrl, data.toString(), { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }); + + const accessToken = response.data.access_token; + // console.log('Access Token:', accessToken); + return accessToken; + } catch (error) { + console.error('Error fetching access token:', error.response ? error.response.data : error.message); + } + return '' +} +@injectable({scope: BindingScope.TRANSIENT}) +export class MobService { + constructor(/* Add @inject to inject parameters */) {} + + // TODO: handle errors ? + public async subscriptionsFind(incentiveId?: string, status?: string) { + const headers = {Authorization: `Bearer ${await getAccessToken()}`} + const params : {incentiveId?: string, status?: string} = {} + if (incentiveId) { + params.incentiveId = incentiveId + } + if (status) { + params.status = status + } + const response = await axios.get(process.env.MOB_API_SUBSCRIPTION_URL ?? 'http://localhost:3000/v1/subscriptions', {headers: headers, params: params}) + return response.data as Subscription[] + } +} diff --git a/afapast/src/strategies/api-key.strategy.ts b/afapast/src/strategies/api-key.strategy.ts new file mode 100644 index 0000000..7c78308 --- /dev/null +++ b/afapast/src/strategies/api-key.strategy.ts @@ -0,0 +1,21 @@ +import {AuthenticationStrategy} from '@loopback/authentication'; +import {Request} from 'express'; +import {AuthenticationService} from '../services/authentication.service'; +import {service} from '@loopback/core'; +import { UserProfile, securityId } from '@loopback/security'; + +export class ApiKeyAuthenticationStrategy implements AuthenticationStrategy { + name : string = 'api-key'; + + constructor( + @service(AuthenticationService) + private authenticationService: AuthenticationService, + ) {} + + async authenticate(request: Request): Promise<UserProfile | undefined> { + const apiKey: string = this.authenticationService.extractApiKey(request); + const apiUser : UserProfile = {[securityId]: '', key: apiKey} + + return apiUser; + } +} diff --git a/afapast/templates/commons/button.ejs b/afapast/templates/commons/button.ejs new file mode 100644 index 0000000..a58d418 --- /dev/null +++ b/afapast/templates/commons/button.ejs @@ -0,0 +1,59 @@ +<table + style=" + margin: 20px auto; + padding: 20px 0px; + text-align: center; + width: auto; + max-width: 640px; + " +> + <tr> + <td> + <!--[if mso]> + <v:roundrect + xmlns:v="urn:schemas-microsoft-com:vml" + xmlns:w="urn:schemas-microsoft-com:office:word" + href="<%= link %>" + style="mso-wrap-style: none; mso-position-horizontal: center" + arcsize="50%" + fillcolor="#01bf7d" + stroke="false" + > + <v:textbox style="mso-fit-shape-to-text: true"> + <center + style=" + color: #ffffff; + font-family: sans-serif; + font-size: 13px; + font-weight: bold; + " + > + <%= text %> + </center></v:textbox + ></v:roundrect + > + <![endif]--> + <!--[if !mso]> <!--> + <a + href="<%= link %>" + style=" + background-color: #01bf7d; + color: #ffffff; + font-weight: bold; + text-decoration: none; + text-align: center; + border-radius: 25px; + -webkit-border-radius: 25px; + -moz-border-radius: 25px; + display: inline-block; + padding: 0.05in 0.1in; + font-family: sans-serif; + font-size: 13px; + box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px; + padding: 12px 20px; + " + ><%= text %></a + ><!-- <![endif]--> + </td> + </tr> +</table> diff --git a/afapast/templates/commons/footer.ejs b/afapast/templates/commons/footer.ejs new file mode 100644 index 0000000..cc3b1ab --- /dev/null +++ b/afapast/templates/commons/footer.ejs @@ -0,0 +1,22 @@ +<%- include('utils'); %> +<% const websiteLink = getWebsiteLink("mentions-legales-cgu") %> +<% const footerLink = getLink("mob-footer.png") %> +<table + style="height: fit-content;width: 100%;text-align: center;color: white;max-width: 640px;background-color: #464CD0;width:640px;" +> + <tr> + <td> + <p style="margin-top: 15px"> + © Mon Compte Mobilité - Tous droits réservés - + <a style="color: white;" href="<%= websiteLink %>">Mentions légales </a> + </p> + </td> + </tr> + <tr> + <td> + <div style="width: auto; margin: 15px 0px;"> + <img src="<%=footerLink %>" alt="mob footer" height="50"/> + </div> + </td> + </tr> +</table> diff --git a/afapast/templates/commons/header.ejs b/afapast/templates/commons/header.ejs new file mode 100644 index 0000000..a0b84ad --- /dev/null +++ b/afapast/templates/commons/header.ejs @@ -0,0 +1,10 @@ +<%- include('utils'); %> + + <% const headerLink = getLink("logo-with-baseline.png") %> + <table style="margin-bottom: 40px;max-width: 640px;width: 640px;"> + <tr style="text-align: center;"> + <td> + <img style="width: auto;height: 50px;" src="<%=headerLink %>" alt="mob header" /> + </td> + </tr> + </table> \ No newline at end of file diff --git a/afapast/templates/commons/utils.ejs b/afapast/templates/commons/utils.ejs new file mode 100644 index 0000000..fd28978 --- /dev/null +++ b/afapast/templates/commons/utils.ejs @@ -0,0 +1,11 @@ +<% +getLink = function(imgName) { + return `https://static.moncomptemobilite.fr/assets/${imgName}` +} +%> + +<% +getWebsiteLink = function(cgu) { + return `https://static.moncomptemobilite.fr/${cgu}` +} +%> diff --git a/afapast/templates/css/style.ejs b/afapast/templates/css/style.ejs new file mode 100644 index 0000000..2753352 --- /dev/null +++ b/afapast/templates/css/style.ejs @@ -0,0 +1,143 @@ +<style> + * { + font-family: 'sofia-pro', sans-serif; + font-weight: 200; + font-size: 15px; + line-height: 1.5; + overflow-wrap: break-word; + } + + .grid-footer { + display: grid; + grid-template-columns: 600px; + justify-content: center; + height: 150px; + width: 100%; + text-align: center; + padding-top: 30px; + color: white; + margin-top: 20px; + } + + .grid-header { + display: grid; + grid-template-columns: 600px; + justify-content: center; + margin-bottom: 20px; + } + + .grid-container { + display: grid; + grid-template-columns: 600px; + justify-content: center; + } + + .grid-center { + text-align: left; + } + + .header-container { + text-align: center; + } + + .confirm-btn { + color: white; + background-color: #01bf7d; + border-radius: 25px; + outline: none; + padding: 12px 20px; + border: none; + box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px; + } + + .btn { + text-align: center; + } + + .confirm-btn a { + text-decoration: none; + color: white; + } + + .footer-logo { + padding-top: 15px; + width: auto; + height: 50px; + object-fit: contain; + } + + .container { + background-color: #464cd0; + } + + .header-logo { + width: auto; + height: 50px; + } + + .italic { + font-style: italic; + font-weight: bold; + } + + .colorLink { + color: white; + } + + /* Mobile */ + + @media screen and (max-width: 600px) { + .grid-footer { + grid-template-columns: 1fr; + text-align: left !important; + height: 100px; + padding-top: 0; + margin-top: 0; + } + + .grid-header { + grid-template-columns: 1fr; + } + + .grid-container { + grid-template-columns: 1fr; + } + .confirm-btn { + padding: 10px 12px; + font-size: 8px; + } + + .container { + position: relative; + width: 100%; + height: 100%; + } + + .footer-img { + position: absolute; + left: 0; + width: 50%; + margin-left: 30px; + } + + .footer-parag { + position: absolute; + right: 0; + width: 40%; + margin-right: 80px; + } + + .footer-logo { + height: 50px; + padding-top: 20px; + } + } + + @media screen and (max-width: 438px) { + .footer-parag { + width: 50%; + font-size: 12px; + margin-right: 60px; + } + } +</style> diff --git a/afapast/templates/voucher-airweb.ejs b/afapast/templates/voucher-airweb.ejs new file mode 100644 index 0000000..b54d192 --- /dev/null +++ b/afapast/templates/voucher-airweb.ejs @@ -0,0 +1,22 @@ +<%- include('css/style'); %> <%- include('commons/header'); %> + +<table style="max-width: 640px; width: 640px"> + <tr> + <td> + <div style="margin-bottom: 40px"> + <div> + <p>Bonjour <%= username %>,</p> + <p> + Voici votre code promo à utiliser lors de l'achat de votre titre de transport chez Airweb : <%= voucher %> + </p> + <div> + <p>Merci pour votre confiance,</p> + <p>L’équipe Mon Compte Mobilité</p> + </div> + </div> + </div> + </td> + </tr> +</table> + +<%- include('commons/footer'); %> diff --git a/afapast/tsconfig.json b/afapast/tsconfig.json new file mode 100644 index 0000000..c7b8e49 --- /dev/null +++ b/afapast/tsconfig.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "@loopback/build/config/tsconfig.common.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"] +} From c314261bb04a452114338ac6b6a8c4cc8058b963 Mon Sep 17 00:00:00 2001 From: TTalex <alex.bourreau@free.fr> Date: Fri, 11 Oct 2024 16:54:39 +0200 Subject: [PATCH 03/10] Add amount to voucher model, add example helper script --- afapast/helper_scripts/README.md | 3 +++ .../helper_scripts/add_vouchers_from_csv.py | 22 +++++++++++++++++++ .../helper_scripts/voucher_csv_exemple.csv | 2 ++ afapast/src/models/voucher.model.ts | 9 ++++++++ 4 files changed, 36 insertions(+) create mode 100644 afapast/helper_scripts/README.md create mode 100644 afapast/helper_scripts/add_vouchers_from_csv.py create mode 100644 afapast/helper_scripts/voucher_csv_exemple.csv diff --git a/afapast/helper_scripts/README.md b/afapast/helper_scripts/README.md new file mode 100644 index 0000000..934e88d --- /dev/null +++ b/afapast/helper_scripts/README.md @@ -0,0 +1,3 @@ +Example scripts used to talk to the API. + +This folder is unused during deployement. diff --git a/afapast/helper_scripts/add_vouchers_from_csv.py b/afapast/helper_scripts/add_vouchers_from_csv.py new file mode 100644 index 0000000..728667a --- /dev/null +++ b/afapast/helper_scripts/add_vouchers_from_csv.py @@ -0,0 +1,22 @@ +import csv +import requests + +CSV_FILE = "voucher_csv_exemple.csv" +API = "http://localhost:3002/vouchers" +API_KEY = "apikey" + +headers = { + "X-API-Key": API_KEY +} +with open(CSV_FILE, 'r') as csvfile: + reader = csv.DictReader(csvfile, delimiter=',') + for line in reader: + json_params = { + "value": line['Code'], + "amount": line['Value'] + } + res = requests.post(API, json=json_params, headers=headers) + if res.status_code < 300: + print(res.json()) + else: + print("Error, " + res.text) \ No newline at end of file diff --git a/afapast/helper_scripts/voucher_csv_exemple.csv b/afapast/helper_scripts/voucher_csv_exemple.csv new file mode 100644 index 0000000..45bfa65 --- /dev/null +++ b/afapast/helper_scripts/voucher_csv_exemple.csv @@ -0,0 +1,2 @@ +Code,Voucher Type,Value,Discount Type +4A2NN3ES,DISCOUNT_VOUCHER,30.00,AMOUNT diff --git a/afapast/src/models/voucher.model.ts b/afapast/src/models/voucher.model.ts index 5baa4dd..eb50c48 100644 --- a/afapast/src/models/voucher.model.ts +++ b/afapast/src/models/voucher.model.ts @@ -17,6 +17,9 @@ export class Voucher extends Entity { @property({ type: 'string', required: true, + index: { + unique: true + } }) value: string; @@ -29,6 +32,12 @@ export class Voucher extends Entity { }) status: VOUCHER_STATUS; + @property({ + type: 'string', + default: '' + }) + amount?: string; + @property({ type: 'string', default: '' From 1ba99173fef7b9740effe22efcd51c548f289354 Mon Sep 17 00:00:00 2001 From: TTalex <alex.bourreau@free.fr> Date: Wed, 30 Oct 2024 16:40:52 +0100 Subject: [PATCH 04/10] added pdf certificate to afapast email --- afapast/package.json | 7 ++- afapast/src/__tests__/pdf-gen-test.ts | 37 +++++++++++++ afapast/src/cronjob/handling.cronjob.ts | 13 ++++- afapast/src/services/mail.service.ts | 3 +- afapast/src/services/mob.service.ts | 5 +- afapast/src/utils/mob.png | Bin 0 -> 3811 bytes afapast/src/utils/pdf-certificate-gen.ts | 65 +++++++++++++++++++++++ 7 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 afapast/src/__tests__/pdf-gen-test.ts create mode 100644 afapast/src/utils/mob.png create mode 100644 afapast/src/utils/pdf-certificate-gen.ts diff --git a/afapast/package.json b/afapast/package.json index d8101f6..9f9e9a2 100644 --- a/afapast/package.json +++ b/afapast/package.json @@ -13,6 +13,7 @@ }, "scripts": { "build": "lb-tsc", + "postbuild": "npm run copy-png-files", "build:watch": "lb-tsc --watch", "lint": "npm run eslint && npm run prettier:check", "lint:fix": "npm run eslint:fix && npm run prettier:fix", @@ -34,7 +35,8 @@ "prestart": "npm run rebuild", "start": "node -r source-map-support/register .", "clean": "lb-clean dist *.tsbuildinfo .eslintcache", - "rebuild": "npm run clean && npm run build" + "rebuild": "npm run clean && npm run build", + "copy-png-files": "copyfiles -u 1 src/**/**.png dist/" }, "repository": { "type": "git", @@ -58,10 +60,12 @@ "@loopback/rest": "^14.0.6", "@loopback/rest-explorer": "^7.0.6", "@loopback/service-proxy": "^7.0.6", + "copyfiles": "^2.4.1", "ejs": "^3.1.10", "loopback-connector-openapi": "^6.2.0", "loopback-connector-sqlite3": "^3.0.0", "nodemailer": "^6.9.15", + "pdfkit": "^0.15.0", "sqlite3": "^5.1.7", "tslib": "^2.0.0" }, @@ -72,6 +76,7 @@ "@types/ejs": "^3.1.5", "@types/node": "^16.18.101", "@types/nodemailer": "^6.4.16", + "@types/pdfkit": "^0.13.5", "eslint": "^8.57.0", "source-map-support": "^0.5.21", "typescript": "~5.2.2" diff --git a/afapast/src/__tests__/pdf-gen-test.ts b/afapast/src/__tests__/pdf-gen-test.ts new file mode 100644 index 0000000..3e8206a --- /dev/null +++ b/afapast/src/__tests__/pdf-gen-test.ts @@ -0,0 +1,37 @@ +import { expect } from '@loopback/testlab'; +import { Subscription, SUBSCRIPTION_STATUS } from '../services'; +import { generateCertificatePdf } from '../utils/pdf-certificate-gen' +import fs from "fs" + +describe('pdf-certificate-gen', () => { + const subscription : Subscription = { + id: '66e449e9b26ab5652cefa0cc', + incentiveId: '66e3290d74df3754b04fab9f', + funderName: 'La Fabrique des Mobilités', + incentiveType: 'AideEmployeur', + incentiveTitle: 'Aide employeur', + incentiveTransportList: [ 'transportsCommun' ], + citizenId: '802ac8b1-cde8-42f2-b310-a1c0380018d8', + lastName: 'BOUCHON', + firstName: 'Didier Lucien Gabriel', + email: 'test@yopmail.com', + city: 'Paris', + postcode: '75000', + birthdate: '1961-10-18T00:00:00.000Z', + status: SUBSCRIPTION_STATUS.VALIDATED, + createdAt: '2024-09-13T14:19:21.361Z', + updatedAt: '2024-09-13T14:21:57.874Z', + funderId: '2219e721-ac71-4a65-a202-37675c74ba58', + subscriptionValidation: { mode: 'aucun' }, + specificFields: { textelibre: 'libre' }, + isCitizenDeleted: false, + enterpriseEmail: 'willnotworkanyway@yopmail.com', + consent: true + } + + it('should generate a pdf buffer', async () => { + const pdfBuffer = await generateCertificatePdf(subscription) + expect(pdfBuffer).not.to.be.null + // fs.writeFileSync('attestation_employeur.pdf', pdfBuffer) + }); +}); diff --git a/afapast/src/cronjob/handling.cronjob.ts b/afapast/src/cronjob/handling.cronjob.ts index bd51d19..e71055f 100644 --- a/afapast/src/cronjob/handling.cronjob.ts +++ b/afapast/src/cronjob/handling.cronjob.ts @@ -6,6 +6,7 @@ import { repository } from '@loopback/repository'; import { TrackedIncentivesRepository, VoucherRepository } from '../repositories'; import { VOUCHER_STATUS } from '../models/voucher.model'; import { MailService } from '../services/mail.service'; +import { generateCertificatePdf } from '../utils/pdf-certificate-gen'; function capitalize(str: string) { return str.charAt(0).toUpperCase() + str.slice(1); @@ -80,11 +81,21 @@ export class HandlingCronJob extends CronJob { const voucher = vouchers[0] console.debug("Got an unused voucher : " + voucher.id) + // Employer certifcate pdf + const pdfCertificateBuffer = await generateCertificatePdf(subscription) + const attachements = [ + { + filename: 'attestation_employeur.pdf', + content: pdfCertificateBuffer, + contentType: 'application/pdf', + } + ] + // MAIL ! await this.mailService.sendMailAsHtml(subscription.email, "Votre bon de réduction Airweb", "voucher-airweb", { username: capitalize(subscription.firstName), voucher: voucher.value - }) + }, attachements) console.debug("mail sent to " + subscription.email) // If mail ok, mark the voucher as used diff --git a/afapast/src/services/mail.service.ts b/afapast/src/services/mail.service.ts index 8da6a6b..25c02ed 100644 --- a/afapast/src/services/mail.service.ts +++ b/afapast/src/services/mail.service.ts @@ -28,7 +28,7 @@ export class MailService { * @param templateName ejs template to be used * @param data dict with params used in template */ - async sendMailAsHtml(to: string, subject: string, templateName: string, data?: Object): Promise<any> { + async sendMailAsHtml(to: string, subject: string, templateName: string, data?: Object, attachements?: Array<Object>): Promise<any> { const html = await generateTemplateAsHtml(templateName, data); const mailerInfos = this.mailConfig.configMailer(); await mailerInfos.mailer.sendMail({ @@ -36,6 +36,7 @@ export class MailService { to: to, subject: subject, html: html, + attachments: attachements }); } } diff --git a/afapast/src/services/mob.service.ts b/afapast/src/services/mob.service.ts index c2c25bc..d271e94 100644 --- a/afapast/src/services/mob.service.ts +++ b/afapast/src/services/mob.service.ts @@ -1,14 +1,14 @@ import axios from 'axios'; import {injectable, BindingScope} from '@loopback/core'; -enum SUBSCRIPTION_STATUS { +export enum SUBSCRIPTION_STATUS { ERROR = 'ERREUR', TO_PROCESS = 'A_TRAITER', VALIDATED = 'VALIDEE', REJECTED = 'REJETEE', DRAFT = 'BROUILLON', } -type Subscription = { +export type Subscription = { id: string; incentiveId: string; funderName: string; @@ -31,6 +31,7 @@ type Subscription = { specificFields?: {[prop: string]: any}; isCitizenDeleted: boolean; enterpriseEmail?: string; + subscriptionValidation?: object; } // TODO: move params & URLs to env diff --git a/afapast/src/utils/mob.png b/afapast/src/utils/mob.png new file mode 100644 index 0000000000000000000000000000000000000000..5a89b7c14baceef52d13e1b8fe56589a7828bcd8 GIT binary patch literal 3811 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 literal 0 HcmV?d00001 diff --git a/afapast/src/utils/pdf-certificate-gen.ts b/afapast/src/utils/pdf-certificate-gen.ts new file mode 100644 index 0000000..ed20cda --- /dev/null +++ b/afapast/src/utils/pdf-certificate-gen.ts @@ -0,0 +1,65 @@ +import { Subscription } from '../services/mob.service'; +import PDFDocument from 'pdfkit' +import { PassThrough } from 'stream' +import path from 'path'; + + +const generateHeader = (doc: PDFKit.PDFDocument) => { + const imagePath = path.join(__dirname, '../utils/mob.png'); + doc + .image(imagePath, 50, 45, { width: 50 }) + .fillColor("#444444") + .fontSize(15) + .text("Mon Compte Mobilité", 110, 57) + .fontSize(10) + .text("https://moncomptemobilite.fr", 200, 50, { align: "right", link: "https://moncomptemobilite.fr" }) + .moveDown(); + } +const generateHr = (doc: PDFKit.PDFDocument, y: number) => { + doc + .strokeColor("#aaaaaa") + .lineWidth(1) + .moveTo(50, y) + .lineTo(550, y) + .stroke(); +} + +export const generateCertificatePdf = async (subscription: Subscription) => { + const doc = new PDFDocument(); + + // Pipe the PDF document to a PassThrough stream + const stream = new PassThrough(); + doc.pipe(stream); + + // Add content to the PDF + generateHeader(doc) + doc + .fillColor("#444444") + .fontSize(15) + .text("Objet: Attestation employeur de " + subscription.firstName + " " + subscription.lastName, 50, 160); + + generateHr(doc, 185); + doc.moveDown() + doc + .fontSize(10) + .text("Ce document est généré automatiquement par le service Mon Compte Mobilité suite à une validation manuelle par un responsable de " + subscription.funderName + ".", 50) + .moveDown() + + .text("Il agit de fait comme une attestation que " + subscription.firstName + " " + subscription.lastName + " est actuellement employé chez " + subscription.funderName + ".", 50) + .moveDown() + + .text("Date de la validation : " + new Date(subscription.updatedAt).toLocaleString("FR-fr") + ".", 50) + + + // Finalize the PDF and close the stream + doc.end(); + + // Convert the stream to a buffer + const pdfBuffer : Buffer = await new Promise((resolve, reject) => { + const chunks : any[] = []; + stream.on('data', chunk => chunks.push(chunk)); + stream.on('end', () => resolve(Buffer.concat(chunks))); + stream.on('error', reject); + }); + return pdfBuffer +} \ No newline at end of file From 35f1c28bf9476f2b3fc92ff5700c2428b790c47b Mon Sep 17 00:00:00 2001 From: TTalex <alex.bourreau@free.fr> Date: Wed, 30 Oct 2024 17:09:48 +0100 Subject: [PATCH 05/10] afapast: eslint tweaks --- afapast/src/__tests__/pdf-gen-test.ts | 4 ++-- afapast/src/cronjob/handling.cronjob.ts | 15 ++++++--------- afapast/src/index.ts | 2 +- afapast/src/models/voucher.model.ts | 8 ++++---- afapast/src/services/mail.service.ts | 4 ++-- afapast/src/services/mob.service.ts | 14 +++++++------- afapast/src/utils/pdf-certificate-gen.ts | 2 +- 7 files changed, 23 insertions(+), 26 deletions(-) diff --git a/afapast/src/__tests__/pdf-gen-test.ts b/afapast/src/__tests__/pdf-gen-test.ts index 3e8206a..a883ba2 100644 --- a/afapast/src/__tests__/pdf-gen-test.ts +++ b/afapast/src/__tests__/pdf-gen-test.ts @@ -1,5 +1,5 @@ import { expect } from '@loopback/testlab'; -import { Subscription, SUBSCRIPTION_STATUS } from '../services'; +import { Subscription, SubscriptionStatus } from '../services'; import { generateCertificatePdf } from '../utils/pdf-certificate-gen' import fs from "fs" @@ -18,7 +18,7 @@ describe('pdf-certificate-gen', () => { city: 'Paris', postcode: '75000', birthdate: '1961-10-18T00:00:00.000Z', - status: SUBSCRIPTION_STATUS.VALIDATED, + status: SubscriptionStatus.VALIDATED, createdAt: '2024-09-13T14:19:21.361Z', updatedAt: '2024-09-13T14:21:57.874Z', funderId: '2219e721-ac71-4a65-a202-37675c74ba58', diff --git a/afapast/src/cronjob/handling.cronjob.ts b/afapast/src/cronjob/handling.cronjob.ts index e71055f..86cb92b 100644 --- a/afapast/src/cronjob/handling.cronjob.ts +++ b/afapast/src/cronjob/handling.cronjob.ts @@ -4,7 +4,7 @@ import {CronJob, cronJob} from '@loopback/cron'; import {MobService} from '../services'; import { repository } from '@loopback/repository'; import { TrackedIncentivesRepository, VoucherRepository } from '../repositories'; -import { VOUCHER_STATUS } from '../models/voucher.model'; +import { VoucherStatus } from '../models/voucher.model'; import { MailService } from '../services/mail.service'; import { generateCertificatePdf } from '../utils/pdf-certificate-gen'; @@ -42,15 +42,12 @@ export class HandlingCronJob extends CronJob { private async performJob(): Promise<void> { console.debug("doing the job !") const trackedIncentives = await this.trackedIncentivesRepository.find() - for (let i = 0; i < trackedIncentives.length; i++) { - const trackedIncentive = trackedIncentives[i]; + for (const trackedIncentive of trackedIncentives) { const subscriptions = await this.mobService.subscriptionsFind(trackedIncentive.incentiveId, "VALIDEE") console.debug(subscriptions.length + " subs loaded from api") let nbSubsHandled = 0 - for (let j = 0; j < subscriptions.length; j++) { - const subscription = subscriptions[j]; - + for (const subscription of subscriptions) { if (process.env.MINIMAL_SUBSCRIPTION_START_DATE && subscription.updatedAt < process.env.MINIMAL_SUBSCRIPTION_START_DATE) { // Sub is too old, ignore it continue @@ -69,7 +66,7 @@ export class HandlingCronJob extends CronJob { // Get a voucher const vouchers = await this.voucherRepository.find({ where: { - status: VOUCHER_STATUS.UNUSED + status: VoucherStatus.UNUSED }, limit: 1 }) @@ -100,7 +97,7 @@ export class HandlingCronJob extends CronJob { // If mail ok, mark the voucher as used await this.voucherRepository.updateById(voucher.id, { - status: VOUCHER_STATUS.USED, + status: VoucherStatus.USED, subscriptionId: subscription.id, citizenId: subscription.citizenId, incentiveId: trackedIncentive.incentiveId, @@ -113,7 +110,7 @@ export class HandlingCronJob extends CronJob { await this.trackedIncentivesRepository.updateById(trackedIncentive.id, { lastNbSubs: subscriptions.length, - nbSubsHandled: (trackedIncentive.nbSubsHandled || 0) + nbSubsHandled, + nbSubsHandled: (trackedIncentive.nbSubsHandled ?? 0) + nbSubsHandled, lastReadTime: new Date().toISOString() }) console.debug("Incentive updated, added " + nbSubsHandled + " subs handled") diff --git a/afapast/src/index.ts b/afapast/src/index.ts index c67cb94..574cd5a 100644 --- a/afapast/src/index.ts +++ b/afapast/src/index.ts @@ -18,7 +18,7 @@ if (require.main === module) { const config = { rest: { port: +(process.env.PORT ?? 3002), - host: process.env.HOST || '127.0.0.1', + host: process.env.HOST ?? '127.0.0.1', // The `gracePeriodForClose` provides a graceful close for http/https // servers with keep-alive clients. The default value is `Infinity` // (don't force-close). If you want to immediately destroy all sockets diff --git a/afapast/src/models/voucher.model.ts b/afapast/src/models/voucher.model.ts index eb50c48..921a94e 100644 --- a/afapast/src/models/voucher.model.ts +++ b/afapast/src/models/voucher.model.ts @@ -1,5 +1,5 @@ import {Entity, model, property} from '@loopback/repository'; -export enum VOUCHER_STATUS { +export enum VoucherStatus { UNUSED = "UNUSED", USED = "USED", REVOKED = "REVOKED" @@ -25,12 +25,12 @@ export class Voucher extends Entity { @property({ type: 'string', - default: VOUCHER_STATUS.UNUSED, + default: VoucherStatus.UNUSED, jsonSchema: { - enum: Object.values(VOUCHER_STATUS), + enum: Object.values(VoucherStatus), }, }) - status: VOUCHER_STATUS; + status: VoucherStatus; @property({ type: 'string', diff --git a/afapast/src/services/mail.service.ts b/afapast/src/services/mail.service.ts index 25c02ed..46f7a51 100644 --- a/afapast/src/services/mail.service.ts +++ b/afapast/src/services/mail.service.ts @@ -6,7 +6,7 @@ export const generateTemplateAsHtml = async ( templateName: string, data?: Object | undefined, ): Promise<string> => { - return await ejs.renderFile( + return ejs.renderFile( `./templates/${templateName}.ejs`, data ?? {}, ); @@ -28,7 +28,7 @@ export class MailService { * @param templateName ejs template to be used * @param data dict with params used in template */ - async sendMailAsHtml(to: string, subject: string, templateName: string, data?: Object, attachements?: Array<Object>): Promise<any> { + async sendMailAsHtml(to: string, subject: string, templateName: string, data?: Object, attachements?: Array<Object>) { const html = await generateTemplateAsHtml(templateName, data); const mailerInfos = this.mailConfig.configMailer(); await mailerInfos.mailer.sendMail({ diff --git a/afapast/src/services/mob.service.ts b/afapast/src/services/mob.service.ts index d271e94..3fc5bad 100644 --- a/afapast/src/services/mob.service.ts +++ b/afapast/src/services/mob.service.ts @@ -1,7 +1,7 @@ import axios from 'axios'; import {injectable, BindingScope} from '@loopback/core'; -export enum SUBSCRIPTION_STATUS { +export enum SubscriptionStatus { ERROR = 'ERREUR', TO_PROCESS = 'A_TRAITER', VALIDATED = 'VALIDEE', @@ -24,11 +24,11 @@ export type Subscription = { birthdate: string; communityId?: string; consent: boolean; - status: SUBSCRIPTION_STATUS; + status: SubscriptionStatus; createdAt: string; updatedAt: string; funderId: string; - specificFields?: {[prop: string]: any}; + specificFields?: {[prop: string]: object | string}; isCitizenDeleted: boolean; enterpriseEmail?: string; subscriptionValidation?: object; @@ -37,12 +37,12 @@ export type Subscription = { // TODO: move params & URLs to env async function getAccessToken(): Promise<string> { const tokenUrl = process.env.MOB_TOKEN_URL ?? 'http://localhost:9000/auth/realms/mcm/protocol/openid-connect/token' - const client_id = process.env.MOB_CLIENT_ID ?? 'simulation-maas-backend' - const client_secret = process.env.MOB_CLIENT_SECRET ?? '4x1zfk4p4d7ZdLPAsaWBhd5mu86n5ZWN' + const clientId = process.env.MOB_CLIENT_ID ?? 'simulation-maas-backend' + const clientSecret = process.env.MOB_CLIENT_SECRET ?? '4x1zfk4p4d7ZdLPAsaWBhd5mu86n5ZWN' const data = new URLSearchParams({ grant_type: 'client_credentials', - client_id: client_id, - client_secret: client_secret, + client_id: clientId, + client_secret: clientSecret, }); try { diff --git a/afapast/src/utils/pdf-certificate-gen.ts b/afapast/src/utils/pdf-certificate-gen.ts index ed20cda..141a36c 100644 --- a/afapast/src/utils/pdf-certificate-gen.ts +++ b/afapast/src/utils/pdf-certificate-gen.ts @@ -56,7 +56,7 @@ export const generateCertificatePdf = async (subscription: Subscription) => { // Convert the stream to a buffer const pdfBuffer : Buffer = await new Promise((resolve, reject) => { - const chunks : any[] = []; + const chunks : Uint8Array[] = []; stream.on('data', chunk => chunks.push(chunk)); stream.on('end', () => resolve(Buffer.concat(chunks))); stream.on('error', reject); From 7ca832275340f39fcec7b21d87de35969b643b5e Mon Sep 17 00:00:00 2001 From: TTalex <alex.bourreau@free.fr> Date: Tue, 19 Nov 2024 17:37:48 +0100 Subject: [PATCH 06/10] Afapast: update email and pdf content, added cc capabilities --- afapast/src/cronjob/handling.cronjob.ts | 14 ++++++++- .../src/models/tracked-incentives.model.ts | 7 +++++ afapast/src/utils/pdf-certificate-gen.ts | 3 ++ afapast/templates/rtcr-confirmation.ejs | 30 +++++++++++++++++++ afapast/templates/voucher-airweb.ejs | 3 ++ 5 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 afapast/templates/rtcr-confirmation.ejs diff --git a/afapast/src/cronjob/handling.cronjob.ts b/afapast/src/cronjob/handling.cronjob.ts index 86cb92b..5542289 100644 --- a/afapast/src/cronjob/handling.cronjob.ts +++ b/afapast/src/cronjob/handling.cronjob.ts @@ -88,13 +88,25 @@ export class HandlingCronJob extends CronJob { } ] - // MAIL ! + // MAIL to employee ! await this.mailService.sendMailAsHtml(subscription.email, "Votre bon de réduction Airweb", "voucher-airweb", { username: capitalize(subscription.firstName), voucher: voucher.value }, attachements) console.debug("mail sent to " + subscription.email) + // MAIL to CC if any + if (trackedIncentive.ccContacts) { + const ccContacts = trackedIncentive.ccContacts.split(',') + for (const ccContact of ccContacts) { + await this.mailService.sendMailAsHtml(ccContact, `Notification : nouvelle validation de droit Tiers payant (pour l'employeur ${subscription.funderName})`, "rtcr-confirmation", { + subscription: subscription, + voucher: voucher.value + }, attachements) + console.debug("mail sent to " + ccContact) + } + } + // If mail ok, mark the voucher as used await this.voucherRepository.updateById(voucher.id, { status: VoucherStatus.USED, diff --git a/afapast/src/models/tracked-incentives.model.ts b/afapast/src/models/tracked-incentives.model.ts index e82d792..50739c1 100644 --- a/afapast/src/models/tracked-incentives.model.ts +++ b/afapast/src/models/tracked-incentives.model.ts @@ -32,6 +32,13 @@ export class TrackedIncentives extends Entity { }) nbSubsHandled?: number; + @property({ + type: 'string', + description: 'Contacts that will also receive the emails, alongside the employee. Comma separated list of emails', + default: '', + }) + ccContacts?: string; + constructor(data?: Partial<TrackedIncentives>) { super(data); } diff --git a/afapast/src/utils/pdf-certificate-gen.ts b/afapast/src/utils/pdf-certificate-gen.ts index 141a36c..a057cdd 100644 --- a/afapast/src/utils/pdf-certificate-gen.ts +++ b/afapast/src/utils/pdf-certificate-gen.ts @@ -48,6 +48,9 @@ export const generateCertificatePdf = async (subscription: Subscription) => { .text("Il agit de fait comme une attestation que " + subscription.firstName + " " + subscription.lastName + " est actuellement employé chez " + subscription.funderName + ".", 50) .moveDown() + .text("Cette attestation marque l'autorisation de prise en charge de l'aide " + subscription.incentiveTitle + ", avec la modalité: " + subscription.specificFields!["Type d'abonnement"] + ".", 50) + .moveDown() + .text("Date de la validation : " + new Date(subscription.updatedAt).toLocaleString("FR-fr") + ".", 50) diff --git a/afapast/templates/rtcr-confirmation.ejs b/afapast/templates/rtcr-confirmation.ejs new file mode 100644 index 0000000..a43be7f --- /dev/null +++ b/afapast/templates/rtcr-confirmation.ejs @@ -0,0 +1,30 @@ +<%- include('css/style'); %> <%- include('commons/header'); %> + +<table style="max-width: 640px; width: 640px"> + <tr> + <td> + <div style="margin-bottom: 40px"> + <div> + <p>Bonjour,</p> + <p> + Nous vous informons qu'un gestionnaire de l'entreprise <%= subscription.funderName %> vient de valider une demande. + </p> + <ul> + <li>Salarié concerné : <%= subscription.firstName %> <%= subscription.lastName %></li> + <li>Autorisé à bénéficier de la prise en charge de l'aide <%= subscription.incentiveTitle %></li> + <li>Avec la modalité: <%= subscription.specificFields["Type d'abonnement"] %></li> + </ul> + <p> + Si besoin, vous trouverez ci-joint une attestation employeur reprenant ces informations. + </p> + <div> + <p>Merci pour votre confiance,</p> + <p>L’équipe Mon Compte Mobilité</p> + </div> + </div> + </div> + </td> + </tr> +</table> + +<%- include('commons/footer'); %> diff --git a/afapast/templates/voucher-airweb.ejs b/afapast/templates/voucher-airweb.ejs index b54d192..0e927cc 100644 --- a/afapast/templates/voucher-airweb.ejs +++ b/afapast/templates/voucher-airweb.ejs @@ -9,6 +9,9 @@ <p> Voici votre code promo à utiliser lors de l'achat de votre titre de transport chez Airweb : <%= voucher %> </p> + <p> + Vous trouverez aussi ci-joint votre attestation employeur. Cette dernière pourra vous être demandée lors du parcours d'achat Airweb. + </p> <div> <p>Merci pour votre confiance,</p> <p>L’équipe Mon Compte Mobilité</p> From 28d03bfe2d7f1f554819ab67fb0400e5df6e86f3 Mon Sep 17 00:00:00 2001 From: TTalex <alex.bourreau@free.fr> Date: Tue, 19 Nov 2024 19:13:40 +0100 Subject: [PATCH 07/10] Afapast: added fields descriptions and example scripts --- afapast/helper_scripts/afapast_api_tips.py | 55 +++++++++++++++ .../create_formated_incentive_in_mob.py | 69 +++++++++++++++++++ .../src/models/tracked-incentives.model.ts | 7 +- afapast/src/models/voucher.model.ts | 7 ++ 4 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 afapast/helper_scripts/afapast_api_tips.py create mode 100644 afapast/helper_scripts/create_formated_incentive_in_mob.py diff --git a/afapast/helper_scripts/afapast_api_tips.py b/afapast/helper_scripts/afapast_api_tips.py new file mode 100644 index 0000000..1d6af68 --- /dev/null +++ b/afapast/helper_scripts/afapast_api_tips.py @@ -0,0 +1,55 @@ +# coding: utf-8 + +# This file contains the main examples of API usage as a reminder +# An API explorer is also available at API_HOST/explorer for a complete documentation + + +import requests + +API_HOST = "http://localhost:3002" +API_KEY = "" + +headers = { + "X-API-Key": API_KEY +} + +# Add a new incentive to track +url = API_HOST + "/tracked-incentives" +json_params = { + incentiveId: "67378718fce6b98d279a0f27", # Incentive id, required + ccContacts: "hello@test.com,hello2@test.com", # Comma separated list of emails, optional +} +res = requests.post(url, json=json_params, headers=headers) +print(res.json()) + + +# Check status of a tracked incentive +url = API_HOST + "/tracked-incentives/67378718fce6b98d279a0f27" +res = requests.get(url, headers=headers) +print(res.json()) + +# Expected response: +# { +# "id": 1, +# "incentiveId": "67378718fce6b98d279a0f27", +# "lastReadTime": "2021-03-09T15:00:00.000Z", # Last time the subscriptions were checked +# "lastNbSubs": 0, # Total number of VALIDEE subscriptions during the last check +# "nbSubsHandled": 0, # Total number of subscriptions handled +# "ccContacts": "hello@test.com,hello2@test.com" +# } + +# Check vouchers usage +url = API_HOST + "/vouchers" +res = requests.get(url, headers=headers) +print(res.json()) + +# Expected response: +# [{ +# "id": 1, +# "value": "4A2NN3ES", +# "amount": "30.00", +# "status": "USED", +# "subscriptionId": "66e32c5974df3754b04faba0", +# "citizenId": "802ac8b1-cde8-42f2-b310-a1c0380018d8", +# "incentiveId": "67378718fce6b98d279a0f27" # Incentive id +# }] \ No newline at end of file diff --git a/afapast/helper_scripts/create_formated_incentive_in_mob.py b/afapast/helper_scripts/create_formated_incentive_in_mob.py new file mode 100644 index 0000000..a21c379 --- /dev/null +++ b/afapast/helper_scripts/create_formated_incentive_in_mob.py @@ -0,0 +1,69 @@ +# coding: utf-8 +import requests +import json + +url = "http://localhost:3000/v1/incentives" +# There is no real way of getting this token programmatically +# We suggest logging in to the admin interface, and looking in dev tools for the token query to idp auth/realms/mcm/protocol/openid-connect/token +# The response should contain an access_token usable here +token = "" + +# These are found in the admin interface +funderId = "318d18d5-0d02-457e-918e-8b272251d1b2" +territoryIds = ["66e328c074df3754b04fab9e"] + +title = "DOMICILE- TRAVAIL - prise en charge à 92,5%" +minAmount = "92.5%" +allocatedAmount = "92.5%" + +# title = "Forfait liberté - prise en charge à 75%" +# minAmount = "75%" +# allocatedAmount = "75%" + +payload = json.dumps({ + "title": title, + "description": "Aide Tiers Payant pour l'entreprise Communauté d’Agglomération de La Rochelle\n\nDemande de prise en charge directe de la part employeur, sans avance de frais.\nRèglement du reste à charge par l'employé directement sur la boutique en ligne : boutiqueenligne.fr ou sur l'application 'BOUTIQUE'", + "incentiveType": "AideEmployeur", + "funderId": funderId, + "minAmount": minAmount, + "transportList": [ + "transportsCommun", + "velo" + ], + "territoryIds": territoryIds, + "allocatedAmount": allocatedAmount, + "conditions": "- être employé de Communauté d’Agglomération de La Rochelle", + "paymentMethod": "- Validation par le gestionnaire \n- Application du droit en une fois directement sur la boutique en ligne\nou\n- Règlement en une fois sous la forme d'un coupon valable sur la boutique en ligne", + "contact": "", + "additionalInfos": "Pour plus d'information, consultez l [aide EN LIGNE](https://moncomptemobilite.fr/) ![image TEST](https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSqQXCfw2Ulfrfe1xG2NGkSe7FOnT0h9AEjcQ&s)", + "isMCMStaff": True, + "subscriptionCheckMode": "MANUEL", + "isCitizenNotificationsDisabled": False, +# "subscriptionLink": "", + "specificFields": [ + { + "isRequired": True, + "title": "Type d'abonnement", + "inputFormat": "listeChoix", + "choiceList": { + "possibleChoicesNumber": 1, + "inputChoiceList": [ + { + "inputChoice": "Abonnement Yélo seul" + }, + { + "inputChoice": "Abonnement Yélo + Vélo Libre Service (VLS) à +5 €" + } + ] + } + }, + ], +}) +headers = { + 'Authorization': 'Bearer ' + token, + 'Content-Type': 'application/json' +} + +response = requests.request("POST", url, headers=headers, data=payload) + +print(response.json()) diff --git a/afapast/src/models/tracked-incentives.model.ts b/afapast/src/models/tracked-incentives.model.ts index 50739c1..c55ccdf 100644 --- a/afapast/src/models/tracked-incentives.model.ts +++ b/afapast/src/models/tracked-incentives.model.ts @@ -4,6 +4,7 @@ import {Entity, model, property} from '@loopback/repository'; export class TrackedIncentives extends Entity { @property({ type: 'number', + description: 'Auto generated id', id: true, generated: true, }) @@ -11,30 +12,34 @@ export class TrackedIncentives extends Entity { @property({ type: 'string', + description: 'Id of the incentive to track, should match an id in moB, example: 67378718fce6b98d279a0f27', required: true, }) incentiveId: string; @property({ type: 'date', + description: 'Last time the subscriptions were checked for this incentive', }) lastReadTime?: string; @property({ type: 'number', + description: 'Total number of VALIDEE subscriptions during the last check', default: 0, }) lastNbSubs?: number; @property({ type: 'number', + description: 'Total number of subscriptions handled', default: 0, }) nbSubsHandled?: number; @property({ type: 'string', - description: 'Contacts that will also receive the emails, alongside the employee. Comma separated list of emails', + description: 'Contacts that will also receive the emails, alongside the employee. Comma separated list of emails. Example: "hello@test.com,hello2@test.com"', default: '', }) ccContacts?: string; diff --git a/afapast/src/models/voucher.model.ts b/afapast/src/models/voucher.model.ts index 921a94e..5242063 100644 --- a/afapast/src/models/voucher.model.ts +++ b/afapast/src/models/voucher.model.ts @@ -10,12 +10,14 @@ export class Voucher extends Entity { @property({ type: 'number', id: true, + description: 'Auto generated id', generated: true, }) id: number; @property({ type: 'string', + description: 'Code of the voucher, will be sent to the citizen, example: "4A2NN3ES"', required: true, index: { unique: true @@ -25,6 +27,7 @@ export class Voucher extends Entity { @property({ type: 'string', + description: 'Status of the voucher, UNUSED marks availability for distribution', default: VoucherStatus.UNUSED, jsonSchema: { enum: Object.values(VoucherStatus), @@ -34,24 +37,28 @@ export class Voucher extends Entity { @property({ type: 'string', + description: 'Precision on the amount, unused for now', default: '' }) amount?: string; @property({ type: 'string', + description: 'Automatically filled Id of the subscription the voucher was used for', default: '' }) subscriptionId?: string; @property({ type: 'string', + description: 'Automatically filled Id of the citizen the voucher was distributed to', default: '' }) citizenId?: string; @property({ type: 'string', + description: 'Automatically filled Id of the incentive the voucher was used for', default: '' }) incentiveId?: string; From 90958c7f0488f47cbace58f9a9e0f2d0fa4e041c Mon Sep 17 00:00:00 2001 From: TTalex <alex.bourreau@free.fr> Date: Mon, 25 Nov 2024 16:33:49 +0100 Subject: [PATCH 08/10] Afapast: added employee email to ccemail template --- afapast/templates/rtcr-confirmation.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/afapast/templates/rtcr-confirmation.ejs b/afapast/templates/rtcr-confirmation.ejs index a43be7f..281d490 100644 --- a/afapast/templates/rtcr-confirmation.ejs +++ b/afapast/templates/rtcr-confirmation.ejs @@ -10,7 +10,7 @@ Nous vous informons qu'un gestionnaire de l'entreprise <%= subscription.funderName %> vient de valider une demande. </p> <ul> - <li>Salarié concerné : <%= subscription.firstName %> <%= subscription.lastName %></li> + <li>Salarié concerné : <%= subscription.firstName %> <%= subscription.lastName %> (<%= subscription.email %>)</li> <li>Autorisé à bénéficier de la prise en charge de l'aide <%= subscription.incentiveTitle %></li> <li>Avec la modalité: <%= subscription.specificFields["Type d'abonnement"] %></li> </ul> From 67bf6b22b8100ca03600334b36201990d77d40ae Mon Sep 17 00:00:00 2001 From: TTalex <alex.bourreau@free.fr> Date: Tue, 17 Dec 2024 18:21:40 +0100 Subject: [PATCH 09/10] Moved db to postgre --- afapast/README.md | 5 +++++ afapast/package.json | 3 +-- afapast/src/datasources/db.datasource.ts | 9 +++++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/afapast/README.md b/afapast/README.md index 34d08ed..9da0ae1 100644 --- a/afapast/README.md +++ b/afapast/README.md @@ -41,3 +41,8 @@ Open http://127.0.0.1:3002 in your browser. | MOB_CLIENT_ID | oidc client id to fetch a token | No | 'simulation-maas-backend' | | MOB_CLIENT_SECRET | oidc client secret to fetch a token | No | '4x1zfk4p4d7ZdLPAsaWBhd5mu86n5ZWN' | | MOB_API_SUBSCRIPTION_URL | subscription url | No | 'http://localhost:3000/v1/subscriptions' | +| DB_HOST | database host | No | 'localhost' | +| DB_PORT | database port | No | 5432 | +| DB_SERVICE_USER | database user | No | 'afapast' | +| DB_SERVICE_PASSWORD | database password | No | 'afapast' | +| DB_DATABASE | database name | No | 'afapast_db' | \ No newline at end of file diff --git a/afapast/package.json b/afapast/package.json index 9f9e9a2..eed0d6c 100644 --- a/afapast/package.json +++ b/afapast/package.json @@ -62,8 +62,7 @@ "@loopback/service-proxy": "^7.0.6", "copyfiles": "^2.4.1", "ejs": "^3.1.10", - "loopback-connector-openapi": "^6.2.0", - "loopback-connector-sqlite3": "^3.0.0", + "loopback-connector-postgresql": "^7.1.8", "nodemailer": "^6.9.15", "pdfkit": "^0.15.0", "sqlite3": "^5.1.7", diff --git a/afapast/src/datasources/db.datasource.ts b/afapast/src/datasources/db.datasource.ts index e8b2e45..50fc42e 100644 --- a/afapast/src/datasources/db.datasource.ts +++ b/afapast/src/datasources/db.datasource.ts @@ -3,8 +3,13 @@ import {juggler} from '@loopback/repository'; const config = { name: 'db', - connector: 'loopback-connector-sqlite3', - file: './data/db.sqlite' + connector: 'postgresql', + host: process.env.DB_HOST ?? 'localhost', + port: process.env.DB_PORT ?? 5432, + user: process.env.DB_SERVICE_USER ?? 'afapast', + password: process.env.DB_SERVICE_PASSWORD ?? 'afapast', + database: process.env.DB_DATABASE ?? 'afapast_db', + ssl: null // TODO ? }; // Observe application's life cycle to disconnect the datasource when From cd78189578c623b8940b763de8e5ec28fcc5758a Mon Sep 17 00:00:00 2001 From: TTalex <alex.bourreau@free.fr> Date: Tue, 17 Dec 2024 18:32:55 +0100 Subject: [PATCH 10/10] update readme --- afapast/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/afapast/README.md b/afapast/README.md index 9da0ae1..04de3ec 100644 --- a/afapast/README.md +++ b/afapast/README.md @@ -4,11 +4,9 @@ A voucher distribution tool, reading mob subscription API and sending emails wit Also an API in itself to manage tracked incentives and vouchers. -## Install dependencies - -By default, dependencies were installed when this application was generated. -Whenever dependencies in `package.json` are changed, run the following command: +## Dependencies +Tested on node v20.18.0. Install deps with ```sh npm install ``` @@ -16,7 +14,9 @@ npm install ## Run the application -Migrate the db if it's the first time +Make sure to have the DB env variables set. + +Migrate the db if it's the first time. ```sh yarn migrate ```