Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ DOCS.md
.env*
.flaskenv*
!.env.project
!.env.vault
!.env.vault

1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const dateChangeRoutes = require('./routes/date-change.routes');
const seatRoutes = require('./routes/seat.routes');
const checkinRoutes = require('./routes/checkin.routes');

const { expireHeldBookings } = require("./services/booking.service");
const { expireHeldBookings, autoCompleteFlights } = require("./services/booking.service");
const { autoGenerateFlights } = require("./services/admin/flight.service");
const { checkAndAlertSLABreach } = require("./services/notification.service");
const { runBatch: autoFlightBatch } = require("./services/admin/auto-flight.service");
Expand Down
7 changes: 7 additions & 0 deletions src/config/refund.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,13 @@ const NOTIFICATIONS = {

// Airline cancellation
sendFlightCancelledNotifications: true,

// Flight notifications
sendFlightStatusChangeNotifications: true,
sendFlightDelayNotifications: true,
sendFlightGateChangeNotifications: true,
sendFlightTimeChangeNotifications: true,
sendFlightBaggageChangeNotifications: true,
},

// Admin dashboard notification
Expand Down
74 changes: 74 additions & 0 deletions src/controllers/admin/price-rule.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"use strict";

const priceRuleService = require("../../services/price-rule.service");

/** GET /api/admin/price-rules */
const getAllRules = async (req, res) => {
try {
const data = await priceRuleService.getAllRules();
res.json({ message: "Lấy danh sách price rules thành công", data });
} catch (err) { res.status(400).json({ error: err.message }); }
};

/** GET /api/admin/price-rules/:id */
const getRuleById = async (req, res) => {
try {
const data = await priceRuleService.getRuleById(req.params.id);
res.json({ data });
} catch (err) { res.status(404).json({ error: err.message }); }
};

/** POST /api/admin/price-rules */
const createRule = async (req, res) => {
try {
const data = await priceRuleService.createRule(req.body);
res.status(201).json({ message: "Tạo price rule thành công", data });
} catch (err) { res.status(400).json({ error: err.message }); }
};

/** PUT /api/admin/price-rules/:id */
const updateRule = async (req, res) => {
try {
const data = await priceRuleService.updateRule(req.params.id, req.body);
res.json({ message: "Cập nhật price rule thành công", data });
} catch (err) { res.status(400).json({ error: err.message }); }
};

/** DELETE /api/admin/price-rules/:id */
const deleteRule = async (req, res) => {
try {
const data = await priceRuleService.deleteRule(req.params.id);
res.json(data);
} catch (err) { res.status(400).json({ error: err.message }); }
};

/** PATCH /api/admin/price-rules/:id/toggle */
const toggleRule = async (req, res) => {
try {
const data = await priceRuleService.toggleRule(req.params.id);
res.json(data);
} catch (err) { res.status(400).json({ error: err.message }); }
};

/**
* GET /api/admin/price-rules/preview
* Xem giá sẽ thay đổi thế nào khi áp dụng rule
* Query: ?base_price=1000000&date=2026-01-30&seat_class=economy
*/
const previewPrice = async (req, res) => {
try {
const { base_price, date, seat_class, airline_code, departure_code, arrival_code } = req.query;
if (!base_price || !date) {
return res.status(400).json({ error: "base_price và date là bắt buộc" });
}
const data = await priceRuleService.previewPrice(base_price, date, {
seatClass: seat_class,
airlineCode: airline_code,
departureCode: departure_code,
arrivalCode: arrival_code,
});
res.json({ data });
} catch (err) { res.status(400).json({ error: err.message }); }
};

module.exports = { getAllRules, getRuleById, createRule, updateRule, deleteRule, toggleRule, previewPrice };
42 changes: 42 additions & 0 deletions src/controllers/flight-brand.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"use strict";

const flightBrandService = require("../services/flight-brand.service");

/**
* GET /api/flights/brand-combinations
* Gợi ý kết hợp hãng tối ưu cho khứ hồi
*
* Query params:
* departure_code, arrival_code (bắt buộc)
* departure_date, return_date (bắt buộc - YYYY-MM-DD)
* seat_class (mặc định: economy)
* adults, children, infants (mặc định: 1,0,0)
* limit (mặc định: 5)
*/
const getBrandCombinations = async (req, res) => {
try {
const {
departure_code, arrival_code,
departure_date, return_date,
seat_class = 'economy',
adults = 1, children = 0, infants = 0,
limit = 5,
} = req.query;

const result = await flightBrandService.getBrandCombinations({
departure_code, arrival_code,
departure_date, return_date,
seat_class, adults, children, infants,
limit,
});

res.json({
message: "Gợi ý kết hợp hãng bay thành công",
data: result,
});
} catch (err) {
res.status(400).json({ error: err.message });
}
};

module.exports = { getBrandCombinations };
78 changes: 78 additions & 0 deletions src/queries/price-rule.queries.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"use strict";

// Lấy tất cả rules đang active cho 1 ngày cụ thể
// Dùng khi search flight để tính adjusted_price
const GET_ACTIVE_RULES_FOR_DATE = `
SELECT
id, name, type,
adjustment_type, adjustment_value,
applies_to_class,
applies_to_airline_code,
applies_to_departure_code,
applies_to_arrival_code,
priority
FROM price_rules
WHERE is_active = TRUE
AND start_date <= $1::DATE
AND end_date >= $1::DATE
ORDER BY priority DESC
`;

// CRUD cho admin
const GET_ALL_RULES = `
SELECT * FROM price_rules
ORDER BY start_date DESC, priority DESC
`;

const GET_RULE_BY_ID = `
SELECT * FROM price_rules WHERE id = $1
`;

const INSERT_RULE = `
INSERT INTO price_rules (
name, type, start_date, end_date,
adjustment_type, adjustment_value,
applies_to_class, applies_to_airline_code,
applies_to_departure_code, applies_to_arrival_code,
priority, is_active, created_by
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,'admin')
RETURNING *
`;

const UPDATE_RULE = `
UPDATE price_rules
SET name = COALESCE($2, name),
type = COALESCE($3, type),
start_date = COALESCE($4, start_date),
end_date = COALESCE($5, end_date),
adjustment_type = COALESCE($6, adjustment_type),
adjustment_value = COALESCE($7, adjustment_value),
applies_to_class = $8,
applies_to_airline_code = $9,
applies_to_departure_code= $10,
applies_to_arrival_code = $11,
priority = COALESCE($12, priority),
is_active = COALESCE($13, is_active),
updated_at = NOW()
WHERE id = $1
RETURNING *
`;

const DELETE_RULE = `
DELETE FROM price_rules WHERE id = $1 AND created_by = 'admin' RETURNING id
`;

const TOGGLE_RULE = `
UPDATE price_rules SET is_active = NOT is_active, updated_at = NOW()
WHERE id = $1 RETURNING id, name, is_active
`;

module.exports = {
GET_ACTIVE_RULES_FOR_DATE,
GET_ALL_RULES,
GET_RULE_BY_ID,
INSERT_RULE,
UPDATE_RULE,
DELETE_RULE,
TOGGLE_RULE,
};
64 changes: 41 additions & 23 deletions src/services/admin/flight.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,15 @@ const QS = require("../../queries/schedule.queries");

// ─── Helpers ──────────────────────────────────────────────────────────────────

const VALID_STATUSES = ["scheduled", "delayed", "boarding", "departed", "arrived", "cancelled", "completed"];
const VALID_STATUSES = [
"scheduled",
"delayed",
"boarding",
"departed",
"arrived",
"cancelled",
"completed",
];
const VALID_CLASSES = ["economy", "business", "first"];

const validateFlightInput = (data, isUpdate = false) => {
Expand Down Expand Up @@ -440,10 +448,10 @@ const updateFlightStatus = async (flightId, status, reason = "") => {
// 3. Payload thông báo
const statusLabels = {
scheduled: "Đúng giờ",
delayed: "Bị trễ",
boarding: "Đang lên máy bay",
departed: "Đã khởi hành",
arrived: "Đã hạ cánh",
delayed: "Bị trễ",
boarding: "Đang lên máy bay",
departed: "Đã khởi hành",
arrived: "Đã hạ cánh",
cancelled: "Đã hủy",
completed: "Đã hoàn thành",
};
Expand Down Expand Up @@ -496,10 +504,10 @@ const updateFlightStatus = async (flightId, status, reason = "") => {

const statusLabels = {
scheduled: "Đúng giờ",
delayed: "Bị trễ",
boarding: "Đang lên máy bay",
departed: "Đã khởi hành",
arrived: "Đã hạ cánh",
delayed: "Bị trễ",
boarding: "Đang lên máy bay",
departed: "Đã khởi hành",
arrived: "Đã hạ cánh",
cancelled: "Đã hủy",
completed: "Đã hoàn thành",
};
Expand All @@ -513,18 +521,18 @@ const updateFlightStatus = async (flightId, status, reason = "") => {

// Fire-and-forget — không block response
sendFlightStatusEmail(booking.contact_email, {
contactName: booking.contact_name,
bookingCode: booking.booking_code,
flightNumber: flight.flight_number,
airlineName: fd.airline_name || "",
depCode: fd.departure_code || "",
depCity: fd.departure_city || "",
arrCode: fd.arrival_code || "",
arrCity: fd.arrival_city || "",
contactName: booking.contact_name,
bookingCode: booking.booking_code,
flightNumber: flight.flight_number,
airlineName: fd.airline_name || "",
depCode: fd.departure_code || "",
depCity: fd.departure_city || "",
arrCode: fd.arrival_code || "",
arrCity: fd.arrival_city || "",
departureTime: fd.departure_time || flight.departure_time,
newStatus: status,
statusLabel: statusLabels[status] || status,
reason: reason || "",
newStatus: status,
statusLabel: statusLabels[status] || status,
reason: reason || "",
}).catch((e) => console.error("[AD-05 Email]", e.message));
}
} catch (emailErr) {
Expand Down Expand Up @@ -767,7 +775,15 @@ const toggleAirlineStatus = async (airlineId) => {
// ══════════════════════════════════════════════════════

const getBookings = async (params) => {
const { page = 1, limit = 10, status, trip_type, search, from_date, to_date } = params;
const {
page = 1,
limit = 10,
status,
trip_type,
search,
from_date,
to_date,
} = params;
const offset = (parseInt(page) - 1) * parseInt(limit);
const conditions = [];
const values = [];
Expand All @@ -789,7 +805,9 @@ const getBookings = async (params) => {
values.push(`%${search}%`);
}
if (from_date && to_date) {
conditions.push(`(b.created_at AT TIME ZONE '+07')::date BETWEEN $${idx} AND $${idx + 1}`);
conditions.push(
`(b.created_at AT TIME ZONE '+07')::date BETWEEN $${idx} AND $${idx + 1}`,
);
idx += 2;
values.push(from_date, to_date);
}
Expand Down Expand Up @@ -855,7 +873,7 @@ const getStatistics = async (params) => {

if (from_date && to_date) {
dateValues.push(from_date, to_date);
dateFilter = `AND (created_at AT TIME ZONE '+07')::date BETWEEN $1::date AND $2::date`;
dateFilter = `AND (created_at AT TIME ZONE '+07')::date BETWEEN $1::date AND $2::date`;
bDateFilter = `AND (b.created_at AT TIME ZONE '+07')::date BETWEEN $1::date AND $2::date`;
}

Expand Down
Loading