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..04de3ec
--- /dev/null
+++ b/afapast/README.md
@@ -0,0 +1,48 @@
+# 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.
+
+## Dependencies
+
+Tested on node v20.18.0. Install deps with
+```sh
+npm install
+```
+
+
+## Run the application
+
+Make sure to have the DB env variables set.
+
+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' |
+| 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/data/db.sqlite b/afapast/data/db.sqlite
new file mode 100644
index 0000000..3f63ff0
Binary files /dev/null and b/afapast/data/db.sqlite differ
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/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/) ",
+ "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/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/package.json b/afapast/package.json
new file mode 100644
index 0000000..eed0d6c
--- /dev/null
+++ b/afapast/package.json
@@ -0,0 +1,83 @@
+{
+ "name": "afapast",
+ "version": "0.0.1",
+ "description": "afapast",
+ "keywords": [
+ "loopback-application",
+ "loopback"
+ ],
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "engines": {
+ "node": "18 || 20 || 22"
+ },
+ "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",
+ "prettier:cli": "lb-prettier \"**/*.ts\" \"**/*.js\"",
+ "prettier:check": "npm run prettier:cli -- -l",
+ "prettier:fix": "npm run prettier:cli -- --write",
+ "eslint": "lb-eslint --report-unused-disable-directives .",
+ "eslint:fix": "npm run eslint -- --fix",
+ "pretest": "npm run rebuild",
+ "test": "lb-mocha --allow-console-logs \"dist/__tests__\"",
+ "posttest": "npm run lint",
+ "test:dev": "lb-mocha --allow-console-logs dist/__tests__/**/*.js && npm run posttest",
+ "docker:build": "docker build -t afapast .",
+ "docker:run": "docker run -p 3000:3000 -d afapast",
+ "premigrate": "npm run build",
+ "migrate": "node ./dist/migrate",
+ "preopenapi-spec": "npm run build",
+ "openapi-spec": "node ./dist/openapi-spec",
+ "prestart": "npm run rebuild",
+ "start": "node -r source-map-support/register .",
+ "clean": "lb-clean dist *.tsbuildinfo .eslintcache",
+ "rebuild": "npm run clean && npm run build",
+ "copy-png-files": "copyfiles -u 1 src/**/**.png dist/"
+ },
+ "repository": {
+ "type": "git",
+ "url": ""
+ },
+ "author": "TTalex",
+ "license": "",
+ "files": [
+ "README.md",
+ "dist",
+ "src",
+ "!*/__tests__"
+ ],
+ "dependencies": {
+ "@loopback/authentication": "^11.0.6",
+ "@loopback/authentication-jwt": "^0.15.6",
+ "@loopback/boot": "^7.0.6",
+ "@loopback/core": "^6.1.3",
+ "@loopback/cron": "^0.12.6",
+ "@loopback/repository": "^7.0.6",
+ "@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-postgresql": "^7.1.8",
+ "nodemailer": "^6.9.15",
+ "pdfkit": "^0.15.0",
+ "sqlite3": "^5.1.7",
+ "tslib": "^2.0.0"
+ },
+ "devDependencies": {
+ "@loopback/build": "^11.0.6",
+ "@loopback/eslint-config": "^15.0.4",
+ "@loopback/testlab": "^7.0.6",
+ "@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/public/index.html b/afapast/public/index.html
new file mode 100644
index 0000000..2e1a1e0
--- /dev/null
+++ b/afapast/public/index.html
@@ -0,0 +1,88 @@
+
+
+
+
+ afapast
+
+
+
+
+
+
+
+
+
+
+
+
afapast
+
Version 1.0.0
+
+
+
+
+
+
+
+
+
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 {
+ 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/__tests__/pdf-gen-test.ts b/afapast/src/__tests__/pdf-gen-test.ts
new file mode 100644
index 0000000..a883ba2
--- /dev/null
+++ b/afapast/src/__tests__/pdf-gen-test.ts
@@ -0,0 +1,37 @@
+import { expect } from '@loopback/testlab';
+import { Subscription, SubscriptionStatus } 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: SubscriptionStatus.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/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é ';;
+ }
+
+ 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 []` 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,
+ ): Promise {
+ 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,
+ ): Promise {
+ 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,
+ ): Promise {
+ 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,
+ ): Promise {
+ 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
+ ): Promise {
+ 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 {
+ 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 {
+ 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 {
+ 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,
+ ): Promise {
+ 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,
+ ): Promise {
+ 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,
+ ): Promise {
+ 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,
+ ): Promise {
+ 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
+ ): Promise {
+ 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 {
+ 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 {
+ await this.voucherRepository.replaceById(id, voucher);
+ }
+
+ @del('/vouchers/{id}')
+ @response(204, {
+ description: 'Voucher DELETE success',
+ })
+ async deleteById(@param.path.number('id') id: number): Promise {
+ 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..5542289
--- /dev/null
+++ b/afapast/src/cronjob/handling.cronjob.ts
@@ -0,0 +1,133 @@
+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 { VoucherStatus } 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);
+}
+
+@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 {
+ console.debug("doing the job !")
+ const trackedIncentives = await this.trackedIncentivesRepository.find()
+ 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 (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
+ }
+ // 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: VoucherStatus.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)
+
+ // Employer certifcate pdf
+ const pdfCertificateBuffer = await generateCertificatePdf(subscription)
+ const attachements = [
+ {
+ filename: 'attestation_employeur.pdf',
+ content: pdfCertificateBuffer,
+ contentType: 'application/pdf',
+ }
+ ]
+
+ // 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,
+ 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..50fc42e
--- /dev/null
+++ b/afapast/src/datasources/db.datasource.ts
@@ -0,0 +1,31 @@
+import {inject, lifeCycleObserver, LifeCycleObserver} from '@loopback/core';
+import {juggler} from '@loopback/repository';
+
+const config = {
+ name: 'db',
+ 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
+// 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..574cd5a
--- /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..c55ccdf
--- /dev/null
+++ b/afapast/src/models/tracked-incentives.model.ts
@@ -0,0 +1,56 @@
+import {Entity, model, property} from '@loopback/repository';
+
+@model()
+export class TrackedIncentives extends Entity {
+ @property({
+ type: 'number',
+ description: 'Auto generated id',
+ id: true,
+ generated: true,
+ })
+ id?: number;
+
+ @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. Example: "hello@test.com,hello2@test.com"',
+ default: '',
+ })
+ ccContacts?: string;
+
+ constructor(data?: Partial) {
+ 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..5242063
--- /dev/null
+++ b/afapast/src/models/voucher.model.ts
@@ -0,0 +1,76 @@
+import {Entity, model, property} from '@loopback/repository';
+export enum VoucherStatus {
+ UNUSED = "UNUSED",
+ USED = "USED",
+ REVOKED = "REVOKED"
+}
+
+@model()
+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
+ }
+ })
+ value: string;
+
+ @property({
+ type: 'string',
+ description: 'Status of the voucher, UNUSED marks availability for distribution',
+ default: VoucherStatus.UNUSED,
+ jsonSchema: {
+ enum: Object.values(VoucherStatus),
+ },
+ })
+ status: VoucherStatus;
+
+ @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;
+
+
+ constructor(data?: Partial) {
+ 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 {
+ 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..46f7a51
--- /dev/null
+++ b/afapast/src/services/mail.service.ts
@@ -0,0 +1,42 @@
+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 => {
+ return 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, attachements?: Array