5 |
8 |
12 |
16 |
19 | Envoyez un email à 20 | maxplorateur@gmail.com. 21 | Essayez d'être le plus précis possible (capture d'écran du bug, nom exact 22 | de la gare ...) ! 23 |
24 |28 | Le projet est open source et disponible sur 29 | github. N'hésitez 30 | pas à venir y contribuer ! 🤓 31 |
32 |Votre trajet ${origin} -> ${destination} le ${this.getHumanReadableDate(date)} est disponible en TGVmax !
39 |Départ possible à ${hours.join(' - ')}
40 |Bon voyage !
`, 41 | }; 42 | await this.transport.sendMail(message); // tslint:disable-line 43 | } 44 | 45 | /** 46 | * get human readable date from javascript date object 47 | */ 48 | public getHumanReadableDate(date: Date): string { 49 | return moment(date).locale('fr').format('dddd DD MMMM'); 50 | } 51 | } 52 | 53 | export default new Notification(); 54 | -------------------------------------------------------------------------------- /server/src/errors/DatabaseError.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus } from '../Enum'; 2 | 3 | /** 4 | * database error 5 | */ 6 | export class DatabaseError extends Error { 7 | /** 8 | * error http code 9 | */ 10 | public readonly code: number; 11 | 12 | /** 13 | * error message 14 | */ 15 | public readonly message: string; 16 | 17 | constructor(err: Error) { 18 | super(); 19 | this.code = this.getErrorCode(err); 20 | this.message = this.getErrorMessage(err); 21 | } 22 | 23 | /** 24 | * get http error code from mongo error code 25 | */ 26 | private readonly getErrorCode = (err: unknown): number => { 27 | const mongoError: { code: number; errmsg: string } = err as { code: number; errmsg: string }; 28 | /* tslint:disable */ 29 | if (mongoError.code === 11000) { 30 | return HttpStatus.UNPROCESSABLE_ENTITY; // duplicate unique key 31 | } else { 32 | console.log(err); // unexpected error that needs to be printed 33 | return HttpStatus.INTERNAL_SERVER_ERROR; 34 | } 35 | }; 36 | 37 | /** 38 | * get error message that can be consumed by webapp 39 | * and displayed to final user 40 | */ 41 | private readonly getErrorMessage = (err: unknown): string => { 42 | const mongoError: { code: number; errmsg: string } = err as { code: number; errmsg: string }; 43 | 44 | if (mongoError.code === 11000) { 45 | if (mongoError.errmsg.includes('tgvmaxNumber')) { 46 | return 'Ce numéro TGVmax est déjà utilisé'; 47 | } else if (mongoError.errmsg.includes('email')) { 48 | return 'Cet email est déjà utilisé'; 49 | } else { 50 | return 'Oups, une erreur est survenue ...'; 51 | } 52 | } else { 53 | return 'Oups, une erreur est survenue ...'; 54 | } 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /client/test/AlertDeletion.test.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import Vue from 'vue'; 3 | import Vuex from 'vuex'; 4 | import AlertDeletion from '../src/components/AlertDeletion.vue'; 5 | import vuetify from 'vuetify'; 6 | 7 | describe('AlertDeletion', () => { 8 | let actions; 9 | let store; 10 | 11 | beforeAll(() => { 12 | Vue.use(vuetify); 13 | Vue.use(Vuex); 14 | /** 15 | * mock vuex 16 | */ 17 | actions = { 18 | deleteAlert: jest.fn() 19 | }; 20 | store = new Vuex.Store({ 21 | actions 22 | }); 23 | }); 24 | 25 | it('should emit vuex action "deleteAlert" after click on "supprimer"', async () => { 26 | /** 27 | * mount component AlertDeletion 28 | */ 29 | const wrapper = mount(AlertDeletion, { store }); 30 | expect(wrapper.find('.cardTitle').exists()).toBe(true); 31 | expect(wrapper.find('.cardTitle').text()).toBe('Suppression'); 32 | expect(wrapper.find('.cardText').text()).toBe( 33 | 'Êtes-vous sûr de vouloir supprimer cette alerte ?' 34 | ); 35 | /** 36 | * click button and check event emit 37 | */ 38 | expect(wrapper.emitted()).toEqual({}); 39 | wrapper.find('.deleteBtn').trigger('click'); 40 | expect(actions.deleteAlert).toHaveBeenCalled(); 41 | }); 42 | 43 | it('should emit event "close:dialog" after click on "annuler"', () => { 44 | /** 45 | * mount component AlertDeletion 46 | */ 47 | const wrapper = mount(AlertDeletion); 48 | /** 49 | * click button and check event emit 50 | */ 51 | expect(wrapper.emitted()).toEqual({}); 52 | wrapper.find('.closeBtn').trigger('click'); 53 | expect(wrapper.emitted()).not.toHaveProperty('delete:travelAlert'); 54 | expect(wrapper.emitted()).toHaveProperty('close:dialog'); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /client/test/date.test.js: -------------------------------------------------------------------------------- 1 | import { convertToDatePickerFormat, getFrenchDate, getHour, getISOString } from '../src/helper/date'; 2 | 3 | describe('Date', () => { 4 | it('getFrenchDate - should return proper french date (isostring)', () => { 5 | const input = '2019-09-09T06:55:00Z'; 6 | expect(getFrenchDate(input)).toBe('lundi 9 septembre'); 7 | }); 8 | 9 | it('getFrenchDate - should return proper french date (yyyy-mm-dd)', () => { 10 | const input = '2019-09-10'; 11 | expect(getFrenchDate(input)).toBe('mardi 10 septembre'); 12 | }); 13 | 14 | it('getFrenchDate - should return proper french date (sunday)', () => { 15 | const input = '2019-09-15'; 16 | expect(getFrenchDate(input)).toBe('dimanche 15 septembre'); 17 | }); 18 | 19 | it('getHour - should return proper hour (UTC+2 - summer time)', () => { 20 | const input = '2019-09-10T06:55:00Z'; 21 | expect(getHour(input)).toBe('08:55'); 22 | }); 23 | 24 | it('getHour - should return proper hour (UTC+1 - winter time)', () => { 25 | const input = '2019-10-29T06:55:00Z'; 26 | expect(getHour(input)).toBe('07:55'); 27 | }); 28 | 29 | it('convertToDatePickerFormat - should return proper v-datepicker format', () => { 30 | const input = new Date('2019-10-29T06:55:00Z'); 31 | expect(convertToDatePickerFormat(input)).toBe('2019-10-29'); 32 | }); 33 | 34 | it('getISOString - should return proper isostring (summer time)', () => { 35 | const date = '2019-08-27'; 36 | const time = '22h15'; 37 | expect(getISOString(date, time)).toBe('2019-08-27T20:15:00.000Z'); 38 | }); 39 | 40 | it('getISOString - should return proper isostring (winter time)', () => { 41 | const date = '2019-10-29'; 42 | const time = '22h15'; 43 | expect(getISOString(date, time)).toBe('2019-10-29T21:15:00.000Z'); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /server/src/App.ts: -------------------------------------------------------------------------------- 1 | import * as cors from '@koa/cors'; 2 | import * as koa from 'koa'; 3 | import * as bodyParser from 'koa-bodyparser'; 4 | import * as helmet from 'koa-helmet'; 5 | import * as logger from 'koa-logger'; 6 | import * as Router from 'koa-router'; 7 | import Config from './Config'; 8 | import { HttpStatus } from './Enum'; 9 | import { errorHandler } from './middlewares/errorHandler'; 10 | import StationRouter from './routes/StationRouter'; 11 | import TravelAlertRouter from './routes/TravelAlertRouter'; 12 | import UserRouter from './routes/UserRouter'; 13 | 14 | /** 15 | * CRUD App 16 | */ 17 | class App { 18 | 19 | public readonly app: koa; 20 | 21 | constructor() { 22 | this.app = new koa<{}>(); 23 | this.middleware(); 24 | this.routes(); 25 | } 26 | 27 | /** 28 | * add middlewares 29 | */ 30 | private middleware(): void { 31 | if (process.env.NODE_ENV !== 'test') { 32 | this.app.use(logger()); 33 | } 34 | this.app.use(errorHandler()); 35 | this.app.use(cors({ 36 | origin: Config.whitelist, 37 | })); 38 | this.app.use(helmet()); 39 | this.app.use(bodyParser()); 40 | } 41 | 42 | /** 43 | * add routes 44 | */ 45 | private routes(): void { 46 | const router: Router = new Router<{}>(); 47 | router.get('/', (ctx: koa.Context) => { 48 | ctx.status = HttpStatus.OK; 49 | }); 50 | 51 | this.app.use(router.routes()); 52 | this.app.use(router.allowedMethods()); 53 | this.app.use(StationRouter.routes()); 54 | this.app.use(StationRouter.allowedMethods()); 55 | this.app.use(TravelAlertRouter.routes()); 56 | this.app.use(TravelAlertRouter.allowedMethods()); 57 | this.app.use(UserRouter.routes()); 58 | this.app.use(UserRouter.allowedMethods()); 59 | } 60 | 61 | } 62 | 63 | export default new App().app; 64 | -------------------------------------------------------------------------------- /client/src/components/AlertDeletion.vue: -------------------------------------------------------------------------------- 1 | 2 |16 | {{ this.errorMessage }} 17 |
18 |9 | Maxplorateur analyse sans interruption la disponibilité des TGVmax et vous alerte dès que le votre se libère. 10 |
11 |👌
28 |Vous créez une alerte pour votre trajet. Simple. Gratuit.
29 |🤖
32 |Un robot vérifie toutes les 30 minutes si un billet TGVmax est disponible pour votre trajet.
33 |🚀
36 |Une place disponible a été détectée ? Vous recevez une alerte et pouvez aller réserver votre place !
37 |
2 |
3 | ## Fin de maintenance
4 | ⚠ Ce repo n'est plus maintenu ⚠
5 |
6 | ## Get the best from your TGVmax subscription
7 | *If you’re 16-27 years old and travel at least twice a month with TGV and Intercités, TGVmax is for you. For just €79 per month, you can travel as often as you like.*
8 |
9 | The official definition above used to be true, it was awesome. However it becomes harder and harder to find a TGVmax seat available, especially for people who want to travel friday or sunday evening.
10 |
11 | The process of booking a TGVmax seat now looks like that :
12 | - Connect to oui.sncf at midnight exactly 30 days before the date you want to travel 🕛
13 | - If you're lucky, there is a seat available : book it immediatly and you're done ✅
14 | - Otherwise, a seat may become available at random time during the next 30 days. So you need to connect as often as you can to oui.sncf and hope to find an available seat.
15 |
16 | This process is boring and time consuming. This project is an attempt to make it fully automatic by creating TGVmax alerts.
17 |
18 | ## Understand how it works
19 | Please read documentation [here](./doc/sncf.md)
20 |
21 | ## How to use this project locally ?
22 | ⚠️ This documentation may not be up to date ⚠️
23 |
24 | ### Prerequisites
25 | 1/ Install [MongoDB](https://www.mongodb.com/download-center/community)
26 |
27 | 2/ Install [Docker and Docker Compose](https://docs.docker.com/docker-for-mac/install/)
28 |
29 | ### Run the app locally
30 | 1/ Open a terminal and start your local mongodb server
31 | ```bash
32 | mongodb
33 | ```
34 |
35 | 2/ Open another terminal and go in the project directory (/TGVmax).
36 |
37 | 3/ Build both docker containers
38 | ```bash
39 | docker-compose -f docker-compose.dev.yml build
40 | ```
41 |
42 | 4/ Run both docker compose services and wait a moment
43 | ```bash
44 | docker-compose -f docker-compose.dev.yml up
45 | ```
46 |
47 | 5/ Open your web browser and go to `http://localhost:8080/`. You should see the app running.
48 |
49 | 6/ Create an account. If everything worked well, you should see a new document in your local mongodb database, in the collection *users* 😊.
50 |
--------------------------------------------------------------------------------
/client/src/views/Account.vue:
--------------------------------------------------------------------------------
1 |
2 | Connecté ✅
6 |{{ email }}
13 |{{ tgvmaxNumber }}
20 |Pour vous déconnecter, cliquez sur le bouton ci-dessous
23 |8 | Octobre 2019 9 |
10 |12 | Il faut reconnaitre qu'il est plus simple d'utiliser une application 13 | plutôt qu'un site internet. Pour cette raison, Maxplorateur est 14 | désormais disponible en tant qu'application sur Android et iOS ! 15 | Attention cependant, elle ne se télécharge pas sur l'App Store ou le 16 | Play Store, mais directement depuis votre navigateur web mobile (Chrome, 17 | Safari). Suivez le guide ! 18 |
19 | 20 |23 | Lorsque vous allez sur le site internet maxplorateur.fr avec votre 24 | smartphone, vous devriez voir apparaitre une bannière en bas de page : 25 | "Ajouter Maxplorateur à l'écran d'accueil". Il suffit de cliquer dessus, 26 | puis sur "Ajouter", et c'est bon ! 27 |
28 | 29 |32 | Vous devez vous rendre sur le site maxplorateur.fr en utilisant le 33 | navigateur Safari. De là, cliquez sur l'icône "partager" en 34 | bas de page, descendez puis cliquez sur "Sur l'écran d'accueil". C'est 35 | installé ! 36 |
37 | 38 |40 | Vous devriez désormais voir l'application Maxplorateur sur l'écran 41 | d'accueil de votre smartphone. Notez que l'application se mettra à jour 42 | automatiquement si besoin 😊 43 |
44 |8 | Octobre 2019 9 |
10 |12 | Un billet TGVmax peut se libérer n'importe quand dans les 30 jours 13 | précédant le trajet. Les abonnés ont donc 2 possibilités : se connecter 14 | tous les jours sur oui.sncf pour vérifier la disponibilité, ou alors 15 | mettre une alerte sur maxplorateur.fr ! Suivez le guide pour apprendre à 16 | créer votre première alerte. 17 |
18 | 19 |Pour cela vous aurez besoin d'exactement 3 choses :
21 |22 | 1 - Une adresse email valide. Vous recevrez vos alertes par 23 | email sur cette adresse. 24 |
25 |2 - Un mot de passe.
26 |27 | 3 - Votre numéro d'abonné TGVmax. Celui ci est nécessaire 28 | pour pouvoir vérifier la disponibilité des trajets en TGVmax. Attention 29 | : Un faux numéro ou un numéro érroné ne vous permettra pas de recevoir 30 | des alertes. 31 |
32 | 33 |35 | Rendez vous sur l'onglet "Alertes". En bas à droite de la page, vous 36 | trouverez un bouton "+" qui vous permettra de créer une alerte pour 37 | votre trajet. Pour ceci vous devrez indiquer : 38 |
39 |1 - Votre gare de départ.
40 |2 - Votre gare d'arrivée.
41 |3 - Votre jour de départ.
42 |43 | 4 - Votre heure de départ minimale. C'est à dire l'heure à 44 | partir de laquelle vous serez disponible pour prendre un train le jour 45 | indiqué. 46 |
47 |48 | 5 - Votre heure de départ maximale.C'est à dire l'heure 49 | jusqu'à laquelle vous serez disponible pour prendre un train le jour 50 | indiqué. 51 |
52 |Cliquez ensuite sur "Enregister", et votre alerte sera en place !
53 | 54 |56 | Une fois que vous avez mis votre alerte, un robot va aller vérifier 57 | environ toutes les 30 minutes la disponibilité de votre trajet en 58 | TGVmax. Vous pouvez voir la date à laquelle le robot a vérifié votre 59 | trajet pour la dernière fois en cliquant sur le bouton "INFO" sur votre 60 | alerte. Si le robot voit qu'une place TGVmax s'est libérée pour votre 61 | trajet, il vous envoie un email pour vous le dire ! 62 |
63 |42 | {{ this.errorMessage }} 43 |
44 |52 | {{ this.errorMessage }} 53 |
54 |5 | Aucune alerte en cours 6 |
7 |94 | Avoir un compte vous permet de recevoir par email les alertes de 95 | disponibilité des TGVmax 96 |
97 |6 | Octobre 2019 7 |
8 |10 | Fin janvier 2017, la SNCF lançait en grande pompe son nouvel abonnement 11 | destiné aux 16-27 ans : TGVmax. Le principe ? Pour 79€/mois, un abonné 12 | peut voyager en illimité sur le réseau TGV et intercités. Lorsqu'on sait 13 | qu'un aller simple Paris - Lyon coûte au moins 60€, l'offre semble 14 | prometteuse ... 15 |
16 | 17 |19 | La SNCF a mis le paquet pour promouvoir son nouvel abonnement auprès des 20 | jeunes. On trouve sur leur 21 | site des slogans tels que 22 | : 23 |
24 |VOYAGEZ EN ILLIMITÉ
25 |TGV illimité pour les 16-27 ans
26 |UN MAX DE LIBERTÉ
27 |28 | Mais pour bien comprendre comment ça fonctionne, il faut regarder un peu 29 | plus bas. Tout se joue dans le "J’accepte de voyager hors période de 30 | forte affluence". 31 |
32 |33 | Nous y sommes ! En réalité, l'abonnement TGVmax permet de voyager en 34 | illimité hors période d'affluence. 35 |
36 |37 | La notion de période d'affluence n'est pas très claire. Personne ne sait 38 | exactement ou se situe la limite entre une période de non-affluence et 39 | une période d'affluence. Personne ne sait non plus à l'avance le nombre 40 | de places disponibles dans tel ou tel train. Avec l'expérience, on sait 41 | cependant qu'il n'y a extrêmement peu (pour ne pas dire aucun) de 42 | trajets disponibles en TGVmax pour les week ends prolongés ou les jours 43 | de départ en vacances ... 44 |
45 | 46 |48 | Les billets TGVmax deviennent disponibles exactement 30 jours avant la 49 | date du départ. Pour maximiser ses chances d'avoir un billet TGVmax, il 50 | faut donc se connecter à minuit pile 30 jours avant son trajet. À partir 51 | de là plusieurs cas possibles : 52 |
53 |54 | 1 - Vous avez eu un billet : conservez le pendant 30 jours. Mais 55 | attention, vous ne pouvez en avoir que 6 simultanément ! 56 |
57 |58 | 2 - Vous n'avez pas eu de billet. C'est là que ça devient intéressant. 59 | Le billet peut (mais ce n'est pas sûr) se libérer n'importe quand dans 60 | les 30 jours qui précèdent le départ. Il s'agit alors de se connecter 61 | régulièrement sur l'application de la SNCF pour vérifier si le billet 62 | TGVmax est disponible. Le tout en espérant que personne ne se connecte 63 | plus fréquemment que vous et ne réserve le billet avant vous. 64 |
65 | 66 |68 | On est d'accord que dis comme ça, ça semble galère. Ça n'a d'ailleurs 69 | pas échappé aux principaux médias français qui n'ont pas hésité à tacler 70 | la SNCF à ce sujet. Trois exemples : 71 |
72 |73 | Nouvelobs : J'ai testé l'abonnement TGVMax pendant 3 mois. 74 | Je ne suis pas près de reprendre le train 75 |
76 |77 | Capital : La grogne grandit contre l'offre TGVmax de la 78 | SNCF 79 |
80 |60millions-mag : TGVmax, les limites du train illimité
81 |83 | Et pourtant, malgré toutes ces conditions et ces critiques, TGVmax peut 84 | être extrêmement rentable ! Voici quelques exemples pour vous aider à 85 | faire votre choix. 86 |
87 | 88 |90 | 1 - Vous prenez le train moins de 2 fois par mois. Une carte jeune fera 91 | l'affaire et vous coutera moins cher. 92 |
93 |94 | 2 - Vous ne voyagez que pendant les vacances. Un abonnement TGVmax vous 95 | engage pour 3 mois. La résiliation coûte 15€ si vous le gardez moins 96 | d'un an. 97 |
98 |99 | 3 - Vous n'êtes pas flexible. Un week end de prévu du vendredi soir au 100 | dimanche soir ? Vous n'aurez peut être des billets TGVmax que le samedi 101 | et lundi matin ... 102 |
103 | 104 |106 | 1 - Vous pouvez voyager en semaine. Vous trouverez très souvent des 107 | billets en semaine le mardi, mercredi, jeudi ainsi que le lundi 108 | après-midi et vendredi matin. 109 |
110 |111 | 2 - Vous êtes débrouillard. Pas de Paris -> Lyon de disponible ? Un 112 | Paris -> Valence fera l'affaire, vous ferez le Valence -> Lyon en 113 | covoit' ou en TER, ça sera toujours beaucoup moins cher. 114 |
115 |116 | 3 - Vous voyagez beaucoup. Vous n'aurez peut être pas toujours un billet 117 | TGVmax, mais au global ce sera rentable. 118 |
119 | 120 |122 | TGVmax, c'est la liberté et le train en illimité oui, mais pas sans 123 | condition ! Quelqu'un de débrouillard et de flexible rentabilisera des 124 | centaines de fois sont abonnement, mais tout le monde n'y trouvera pas 125 | son compte ... 126 |
127 |136 | {{ this.errorMessage }} 137 |
138 |