new
This commit is contained in:
parent
ba4e173c9f
commit
e93f945df3
File diff suppressed because one or more lines are too long
@ -34,6 +34,25 @@ const config = {
|
|||||||
clientId: process.env.MS_CLIENT_ID || '',
|
clientId: process.env.MS_CLIENT_ID || '',
|
||||||
clientSecret: process.env.MS_CLIENT_SECRET || '',
|
clientSecret: process.env.MS_CLIENT_SECRET || '',
|
||||||
},
|
},
|
||||||
|
amadeus: {
|
||||||
|
clientId: process.env.AMADEUS_CLIENT_ID || 'YnlNGEWG0bMODXXFh59aWczdKy2Abr3l',
|
||||||
|
clientSecret: process.env.AMADEUS_CLIENT_SECRET || 'YaSiX7mGOu3dhDz6',
|
||||||
|
baseURL: 'https://test.api.amadeus.com',
|
||||||
|
},
|
||||||
|
|
||||||
|
tap: {
|
||||||
|
secretKey: process.env.TAP_SECRET_KEY || 'sk_test_XKokBfNWv6FIYuTMg5LPjhJ',
|
||||||
|
publishableKey: process.env.TAP_PUBLISHABLE_KEY || 'pk_test_EtHFV4BuPQokJT6jiROls87Y',
|
||||||
|
merchantId: process.env.TAP_MERCHANT_ID || '26374580',
|
||||||
|
baseURL: 'https://api.tap.company',
|
||||||
|
successUrl: process.env.TAP_SUCCESS_URL || 'https://flyaru.com/payment-success',
|
||||||
|
failureUrl: process.env.TAP_FAILURE_URL || 'https://flyaru.com/payment-failed',
|
||||||
|
webhookUrl: process.env.TAP_WEBHOOK_URL || 'https://flyaru.com/api/webhook',
|
||||||
|
force3DSecure: true,
|
||||||
|
capture: 'capture',
|
||||||
|
refundWindowDays: 30,
|
||||||
|
},
|
||||||
|
|
||||||
uploadDir: os.tmpdir(),
|
uploadDir: os.tmpdir(),
|
||||||
email: {
|
email: {
|
||||||
from: 'FlyAru-Powered by Arwa Travel Group <app@flatlogic.app>',
|
from: 'FlyAru-Powered by Arwa Travel Group <app@flatlogic.app>',
|
||||||
|
|||||||
@ -36,6 +36,8 @@ const permissionsRoutes = require('./routes/permissions');
|
|||||||
const agenciesRoutes = require('./routes/agencies');
|
const agenciesRoutes = require('./routes/agencies');
|
||||||
|
|
||||||
const cartRoutes = require('./routes/cart');
|
const cartRoutes = require('./routes/cart');
|
||||||
|
const amadeusRoutes = require('./routes/amadeus');
|
||||||
|
|
||||||
|
|
||||||
const getBaseUrl = (url) => {
|
const getBaseUrl = (url) => {
|
||||||
if (!url) return '';
|
if (!url) return '';
|
||||||
@ -150,6 +152,15 @@ app.use(
|
|||||||
cartRoutes,
|
cartRoutes,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Amadeus flight search routes
|
||||||
|
app.use(
|
||||||
|
'/api/amadeus',
|
||||||
|
passport.authenticate('jwt', { session: false }),
|
||||||
|
amadeusRoutes
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
'/api/openai',
|
'/api/openai',
|
||||||
passport.authenticate('jwt', { session: false }),
|
passport.authenticate('jwt', { session: false }),
|
||||||
|
|||||||
30
backend/src/routes/amadeus.js
Normal file
30
backend/src/routes/amadeus.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const wrapAsync = require('../helpers').wrapAsync;
|
||||||
|
const passport = require('passport');
|
||||||
|
const AmadeusService = require('../services/amadeusService');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @route GET /api/amadeus/flights
|
||||||
|
* @desc Search flights via Amadeus
|
||||||
|
* Query params: originLocationCode, destinationLocationCode, departureDate, adults, currency, etc.
|
||||||
|
*/
|
||||||
|
router.get(
|
||||||
|
'/flights',
|
||||||
|
passport.authenticate('jwt', { session: false }),
|
||||||
|
wrapAsync(async (req, res) => {
|
||||||
|
const params = {
|
||||||
|
originLocationCode: req.query.originLocationCode,
|
||||||
|
destinationLocationCode: req.query.destinationLocationCode,
|
||||||
|
departureDate: req.query.departureDate,
|
||||||
|
adults: req.query.adults || 1,
|
||||||
|
max: req.query.max || 10,
|
||||||
|
currency: req.query.currency || 'SAR'
|
||||||
|
};
|
||||||
|
const result = await AmadeusService.searchFlights(params);
|
||||||
|
res.json(result);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@ -4,6 +4,12 @@ const CartService = require('../services/cart');
|
|||||||
const CartDBApi = require('../db/api/cart');
|
const CartDBApi = require('../db/api/cart');
|
||||||
const wrapAsync = require('../helpers').wrapAsync;
|
const wrapAsync = require('../helpers').wrapAsync;
|
||||||
|
|
||||||
|
const tapService = require('../services/tapService');
|
||||||
|
const { v4: uuidv4 } = require('uuid');
|
||||||
|
const db = require('../db/models');
|
||||||
|
const passport = require('passport');
|
||||||
|
|
||||||
|
|
||||||
const config = require('../config');
|
const config = require('../config');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
@ -434,6 +440,45 @@ router.get(
|
|||||||
wrapAsync(async (req, res) => {
|
wrapAsync(async (req, res) => {
|
||||||
const payload = await CartDBApi.findBy({ id: req.params.id });
|
const payload = await CartDBApi.findBy({ id: req.params.id });
|
||||||
|
|
||||||
|
// Checkout endpoint
|
||||||
|
router.post(
|
||||||
|
'/checkout',
|
||||||
|
passport.authenticate('jwt', { session: false }),
|
||||||
|
wrapAsync(async (req, res) => {
|
||||||
|
const currentUser = req.currentUser;
|
||||||
|
const carts = await CartDBApi.findAll({ user: currentUser.id }, null, { currentUser });
|
||||||
|
if (!carts.rows.length) {
|
||||||
|
return res.status(400).json({ message: 'Cart is empty.' });
|
||||||
|
}
|
||||||
|
const cart = carts.rows[0];
|
||||||
|
const items = await db.cartitem.findAll({ where: { cartId: cart.id } });
|
||||||
|
const amount = items.reduce((sum, item) => sum + item.quantity * item.unitprice, 0);
|
||||||
|
const chargeData = {
|
||||||
|
amount,
|
||||||
|
currency: config.tap.currency || 'SAR',
|
||||||
|
threeDSecure: config.tap.force3DSecure,
|
||||||
|
capture: config.tap.capture,
|
||||||
|
description: `Charge for cart ${cart.id}`,
|
||||||
|
reference: {
|
||||||
|
track: uuidv4(),
|
||||||
|
payment: uuidv4(),
|
||||||
|
order: cart.id,
|
||||||
|
},
|
||||||
|
post: { url: config.tap.webhookUrl },
|
||||||
|
redirect: { url: config.tap.successUrl },
|
||||||
|
customer: {
|
||||||
|
id: currentUser.id,
|
||||||
|
email: currentUser.email,
|
||||||
|
first_name: currentUser.firstName,
|
||||||
|
last_name: currentUser.lastName,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const charge = await tapService.createCharge(chargeData);
|
||||||
|
res.json({ charge });
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
res.status(200).send(payload);
|
res.status(200).send(payload);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
51
backend/src/services/amadeusService.js
Normal file
51
backend/src/services/amadeusService.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
const axios = require('axios');
|
||||||
|
const qs = require('qs');
|
||||||
|
const config = require('../config');
|
||||||
|
|
||||||
|
let accessToken = null;
|
||||||
|
let tokenExpiresAt = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtain or refresh Amadeus OAuth token
|
||||||
|
*/
|
||||||
|
async function getAccessToken() {
|
||||||
|
const now = Date.now();
|
||||||
|
if (accessToken && now < tokenExpiresAt) {
|
||||||
|
return accessToken;
|
||||||
|
}
|
||||||
|
const tokenUrl = `${config.amadeus.baseURL}/v1/security/oauth2/token`;
|
||||||
|
const data = qs.stringify({
|
||||||
|
grant_type: 'client_credentials',
|
||||||
|
client_id: config.amadeus.clientId,
|
||||||
|
client_secret: config.amadeus.clientSecret,
|
||||||
|
});
|
||||||
|
const resp = await axios.post(tokenUrl, data, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
accessToken = resp.data.access_token;
|
||||||
|
// expires_in is seconds
|
||||||
|
tokenExpiresAt = now + resp.data.expires_in * 1000 - 60000; // refresh 1 min early
|
||||||
|
return accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search flights via Amadeus
|
||||||
|
* @param {Object} params originLocationCode, destinationLocationCode, departureDate, adults, currency
|
||||||
|
*/
|
||||||
|
async function searchFlights(params) {
|
||||||
|
const token = await getAccessToken();
|
||||||
|
const url = `${config.amadeus.baseURL}/v2/shopping/flight-offers`;
|
||||||
|
const resp = await axios.get(url, {
|
||||||
|
params: params,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return resp.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
searchFlights,
|
||||||
|
};
|
||||||
22
backend/src/services/tapService.js
Normal file
22
backend/src/services/tapService.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
const axios = require('axios');
|
||||||
|
const config = require('../config');
|
||||||
|
|
||||||
|
// Initialize Tap API client
|
||||||
|
const tapClient = axios.create({
|
||||||
|
baseURL: config.tap.baseURL,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${config.tap.secretKey}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a charge via Tap Payments
|
||||||
|
* @param {Object} data - payload for /v2/charges
|
||||||
|
*/
|
||||||
|
async function createCharge(data) {
|
||||||
|
const resp = await tapClient.post('/v2/charges', data);
|
||||||
|
return resp.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { createCharge };
|
||||||
1
frontend/json/runtimeError.json
Normal file
1
frontend/json/runtimeError.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
Loading…
x
Reference in New Issue
Block a user