# Garuda LP ## Overview Components: **Backend** (java) Proccess bookings, payments, handle incoming webhooks. Provide json for UI to consume **Admin** (ruby) Dashboard, reporting, manage all settings, some cron jobs **UI** (nodejs) Stateless app, forward request to backend, and render HTML. Also keep all assets and client side JS **Worker** (ruby) Sending notifications to booking system (amadeus, gos/cos, call_center, etc), run confirmation via altea API (amadeus booking), some other jobs **Scheduler** (java) Legacy app, supposed to be depricated in favor of Worker (some time in future) And: * MySQL * Rabbitmq ## Run Locally **Backend** - requires java 8, gradle. Can run as `./bin/run development`, default port is 8080. Faster development with `cont_compiler` and `cont_runner`: Automatically recompile and restart web server when java files changes. Iteration takes 6-10 secs Run both in separate terminal windows: ``` ./bin/cont_compiler ./bin/cont_runner ``` `cont_compiler` script watching all java files for changes, recompile and send unix signal to running cont_runner script. `cont_runner` script generate class path at first (java cli args with paths to all jars and compiles classes), then run java with that command args. When it receive `SIGHUP` unix signal - restart java process, also can restart by typing `r` to stdin Project have integration tests written in ruby, for all features. **UI** - requires nodejs 8+ and yarn, run as `yarn start`, default port is 3000. Require backend running on port 8080 (default), can try to make test booking by opening http://localhost:3000/payment/test_booking **Admin** - requires ruby 2.3+ Prepare: ``` bundle ``` Run: ``` rails s -p 3002 ``` Admin app and backend and worker connect to same database. Engineering menu: http://localhost:3002/admin/help **Worker** - requires ruby 2.3+, rabbitmq with management plugin Prepare: ``` bundle rake setup_rabbit ``` Run: ``` rails s -p 5000 ``` Don't have own UI but we can see status and logs via admin http://localhost:3002/admin/status_reports/worker_service ## Booking flow ### Amadeus and mobile 1. Amadeus redirect user to landing page with post payload (html form) 2. It contain `CHECKSUM` field equal to `md5(SESSION_ID + MERCHANT_ID + AMOUNT + CURRENCY + RL + USER_EMAIL + ENCRYPTION_KEY)`. encryption_key is a special key from amadeus to verify their payload 3. Landing page create new record in database and redirect user to `/payments/cardinfo/INVOICE_NO`. Invoice_no is `RL + user_txn.id` (auto-increment), inoice is saved in database too 4. For redirected page `/payments/cardinfo/INVOICE_NO` request goes to UI, proxied to backend. Backend give information about booking and determine all possible payment options. UI render html page with booking info, payment forms and prefilled customer details 5. User make payment (more details bellow) 6. User is redirected back to amadeus `CONFIRMATION_URL` or `CANCELLATION_URL` (if payment failed) with form fields: * `STATUS` = `OK` or `KO` * `SITE` = value from initial payload * `LANGUAGE` = from initial payload * `PAYMENT_REFERENCE` aka FOP, info about complete payment, used for recon by garuda * `ACKNOWLEDGEMENT_URL` URL in landing page where amadeus should confirm ticket issuing * `CHECKSUM` = `md5(SESSION_ID + PAYMENT_REFERENCE + ACKNOWLEDGEMENT_URL + ENCRYPTION_KEY)` 7. Amadeus send acknowledgment to landing page 8. Backend mark user_txn as acknowledged 9. Sometimes amadeus doesn't send acknowledgment to landing page and we will send request to `CONFIRMATION_URL` via worker 10. Some payments like bank transfer don't have redirect, and those bookings will be processed via Altea API. It does the same thing: change status of booking in amadeus to paid and provide FOP. 11. Altea API also used to confirm payments when amadeus doesn't send acknowledgment for certain period of time ### GOS, COS, GASAB, TOPUP MILES, CALL CENTER 1. Booking system (gos, cos, gasab, etc) send JSON request to landing page 2. Landing page respond with `{status: 'Success', message: 'Transaction saved', redirect_url: 'https://devpay...'}`. Can try via https://devpay.garuda-indonesia.com/payment/test_booking#gos_json 3. User is redirected to landing page (HTTP GET) 4. User make payment (more details bellow) 5. Landing page redirect user to `CONFIRMATION_URL` or `CANCELLATION_URL` 6. Worker call `NOTIFICATION_URL` (or default value for that partner) 7. Booking system should respond with `{response_code: '00'}` ## Payment Flow Some payment types have different flow: doku credit cards, cybersource credit cards, installments, cc points ### General Types 1. User fill a form and press continue 2. Client side will send ajax request to `GET /payment/cardinfo/XXX/find_similar` to show dialog when user try to book a same ticket (e.g didn't get confirmation email at first time) 3. Check card promo `GET /payment/cardinfo/XXX/card_promo` 4. Submit payment `POST /payment/cardinfo/XXX/pay` 5. Payment response will redirect use to other page or `CONFIRMATION_URL` or show payment form again with error message (some cases can be retried, some cases can not be retried) ### Cubersource Cubersource CC is used for non-IDR payments 1. User fill credit card form 2. client side send request to backend `/api/cardinfo/XXX/cybs` 3. `/cybs` response contain request params including checksum, client side will add card data and submit form redirecting to cybersource. 4. User complete 3D Secure flow (if needed) and redirect back to `/payment/cardinfo/cybsResponse` 5. Landing page process received params and redirect user to `CONFIRMATION_URL` or if payment was rejected it shows payment form again with error message 6. Cybersource will also send PSOT request to `/payment/cardinfo/cybsNotification` ### Doku CC Doku CC is used for IDR payments. Flow is similar with cybersource 1. User fill credit card form 2. Client side send request to `/api/cardinfo/XXX/doku_cc` 3. `/doku_cc` response is combined with card data and submit form redirecting to doku 4. User complete 3DS and redirect back to landing page 5. Redirect back to landing page `/payment/cardinfo/dokuResponse` 6. Doku notification is sent to `/payment/doku_notif` 7. For redirect params and notification we verify data via status API provided by doku. To avoid users fabricating notification params ### CC Points Processed by Midtrans 1. User fill credit card form 2. Client side detect that card's bin can be used for points and enable checkbox 3. User press continue 4. Client side use midtrans.js lib to convert card number to card token (to avoid sending card data to server) 5. Send GET request to `/api/cardinfo/XXX/points` 6. If response have available points then show dialog 7. User confirm how many point they want to redeem 8. Continue with Doku CC flow 9. If Doku transaction wasn't successful then landing page will cancel transaction in midtrans via API ### BNI Mobile 1. User fill form and press continue 2. Send request to doku->BNI which trigger notification to user device 3. Show pending page and continuously check payment status via `GET /api/cardinfo/XXX/state` 4. When payment complete client side will show `alert()` message and redirect user to `CONFIRMATION_URL` 5. If user close browser window then it will complete offline flow similar to bank transfer ## Multi currencies Flow Feature is provided by citibank, communication is done via FIX protocol where then send message encoded via thrift protocol (thrift is basically a json with types and without field names). This FIX + thrift is very developer-not-friendly, and provided jar didn't meet our requirements, so we reverse-engineered it and implemented in ruby. Admin page: https://devpay.garuda-indonesia.com/admin/settings/rates 1. Admin get rates from citibank and save to database 2. Landing page detect if currency conversion can be used for booking 3. User change currency, backend save it to `converted_currency` and `converted_amount` columns 4. User complete payment in new currency 5. At specific time Admin will register those payments in citibank 6. If registering in citibank fails then we send detail by email for manual submitting ## Interesting Features ### Transcript All communication with other systems is recorded in database. Generally we save request details, response details, validation errors (user enter invalid data), some important messages related to booking. Since database is shared, backend & admin & worker write to that table in a same way. Old records are recorded after 1 year (business requirement). That table is basically "insert-only", we have feature to upload all transcripts to google bigquery to reduce mysql database size, but currently disabled. ### Config reloading Many of config values for backend cab be changed in runtime without deployment. Control via: https://devpay.garuda-indonesia.com/admin/settings/properties Internally it save customized values in database and backend process has background thread to fetch properties table and update global config object every few seconds. So customized values will be applied with few seconds delay. ### Piloting We often need to test some features in production before enabling for everyone. All payment types have config variable to enable it, e.g if we set ``` types.linkaja = desktop_pilot ``` Then Linkaja payment type will be shown only on desktop and only for piloting user (with phone number `081122334455`). ### Disabled payment types We can disable any payment type temporary, e.g. when external payment system have technical problems ### Hit Counts We save in database number of hit counts per url per half hour. Also hit counts per booking Get reports at https://devpay.garuda-indonesia.com/admin/reports/hit_counts ## Monitoring Each component has endpoint `/api/ping` and `/api/status`. Ping is to check that app is running, response should be == `pong` Status endpoint give response something like this: ```json { "services": { "backend": { "status": "ok", "time": 0.007768413 }, "worker": { "status": "ok", "time": 0.327572971, "version": "2019_04_01.16_05.d807907" }, "worker_threds": { "status": "ok", "time": 0.327572971 }, "worker_queues": { "status": "ok", "time": 0.327572971 } }, "storages": { "database": { "status": "ok", "time": 0.211680043 } }, "app": { "puma_stats": { "started_at": "2019-12-21T19:01:41Z", "backlog": 0, "running": 2, "pool_capacity": 15, "max_threads": 16, "min_threads": 0 }, "dbpool_stats": { "size": 25, "connections": 2, "busy": 0, "dead": 0, "idle": 2, "waiting": 0, "checkout_timeout": 5 }, "memory": { "bytes": 265760768, "human": "253.449 MB" } }, "system": { "free_space": { "bytes": 69569232896, "human": "64.79 GB" }, "load_averages": "0.21, 0.69, 0.89" }, "version": "2019_11_13.00_10.3947b9d" } ``` `services` is connection to other services that it needs to connect. e.g. `services.backend` is a result of calling `ping` API for backend service, also used for external services. We have [internal tool](https://gamonitor.midtrans.work) to aggregate these values and send us alerts via slack when something goes wrong, but it won't be provided during handover process. I would recommend you to build something similar or use [telegraf](https://github.com/influxdata/telegraf) with json input plugin or use prometheus (require modification of output format). And set charts and alerts via grafana. List of available endpoints: * https://devpay.garuda-indonesia.com/admin/api/ping * https://devpay.garuda-indonesia.com/payment/api/ping * https://devpay.garuda-indonesia.com/payment/api/status * https://devpay.garuda-indonesia.com/payment/ui_api/status * https://devpay.garuda-indonesia.com/admin/api/status Also we have metrics for transaction numbers (URL https://pay.garuda-indonesia.com/admin/api/transactions/count require password), it provide number of transaction matching criteria