Skip to content
Merged

Dev #486

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
1 change: 1 addition & 0 deletions config/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export const TRAVEL_TYPE_FULL = 'full';
// ADMIN ROLES
export const ROLE_SUPER_ADMIN = 'superAdmin';
export const ROLE_ROOM_ADMIN = 'roomAdmin';
export const ROLE_KIOSK_ADMIN = 'kioskAdmin';
export const ROLE_CARD_ADMIN = 'cardAdmin';
export const ROLE_OFFICE_ADMIN = 'officeAdmin';
export const ROLE_ADHYAYAN_ADMIN = 'adhyayanAdmin';
Expand Down
57 changes: 2 additions & 55 deletions controllers/admin/gateManagement.controller.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
import {
GateRecord,
CardDb,
FlatBooking,
RoomBooking
CardDb
} from '../../models/associations.js';
import {
STATUS_MUMUKSHU,
STATUS_GUEST,
STATUS_ONPREM,
STATUS_RESIDENT,
STATUS_SEVA_KUTIR,
STATUS_OFFPREM,
ROOM_STATUS_CHECKEDIN,
ROOM_STATUS_CHECKEDOUT,
ROOM_STATUS_PENDING_CHECKIN
STATUS_OFFPREM
} from '../../config/constants.js';
import logger from '../../config/logger.js';
import database from '../../config/database.js';
import ApiError from '../../utils/ApiError.js';
import Sequelize from 'sequelize';
Expand Down Expand Up @@ -272,38 +266,6 @@ export const gateEntry = async (req, res) => {
{ transaction: t }
);

res.on('finish', async () => {
try {
const flatBooking = await FlatBooking.findOne({
where: {
cardno,
status: ROOM_STATUS_PENDING_CHECKIN,
checkin: { [Sequelize.Op.eq]: moment().format('YYYY-MM-DD') }
}
});

if (flatBooking) {
flatBooking.status = ROOM_STATUS_CHECKEDIN;
await flatBooking.save();
}

const roomBooking = await RoomBooking.findOne({
where: {
cardno,
status: ROOM_STATUS_PENDING_CHECKIN,
checkin: { [Sequelize.Op.eq]: moment().format('YYYY-MM-DD') }
}
});

if (roomBooking) {
roomBooking.status = ROOM_STATUS_CHECKEDIN;
await roomBooking.save();
}
} catch (error) {
logger.error(error);
}
});

await t.commit();
return res.status(200).send({
message: 'Success',
Expand Down Expand Up @@ -346,21 +308,6 @@ export const gateExit = async (req, res) => {
{ transaction: t }
);

const today = moment().format('YYYY-MM-DD');

const booking = await FlatBooking.findOne({
where: {
cardno: req.body.cardno,
status: ROOM_STATUS_CHECKEDIN,
checkout: { [Sequelize.Op.lte]: today }
}
});

if (booking) {
booking.status = ROOM_STATUS_CHECKEDOUT;
await booking.save({ transaction: t });
}

await t.commit();

return res.status(200).send({
Expand Down
279 changes: 279 additions & 0 deletions controllers/admin/roomManagement.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
AC_ROOM_PRICE,
STATUS_CREDITED,
STATUS_PAYMENT_COMPLETED,
STATUS_CASH_COMPLETED,
ERR_TRANSACTION_NOT_FOUND,
AMT_TYPE_LATE_CHECKOUT_ROOM,
STATUS_CONFIRMED,
Expand Down Expand Up @@ -84,6 +85,9 @@ import sequelize from '../../config/database.js';

const CHECKOUT_DEADLINE = '11:00:00';
const LATE_CHECKOUT_HALF = '15:00:00';
// Stays more than 14 days overdue are excluded from kiosk self-checkout
// to prevent stale zombie bookings; staff handles them via the overdue report.
const KIOSK_CHECKEDIN_LOOKBACK_DAYS = 14;

const calcLateCheckoutFee = (roomType, isHalfDay) => {
if (roomType === 'nac') {
Expand Down Expand Up @@ -963,6 +967,178 @@ export const fetchAllFlatBookings = async (req, res) => {
return res.status(200).send({ message: 'Fetched bookings', data: bookings });
};

const kioskCheckoutPreview = async (booking, isFlat, today, nowTime, transaction, bookerNameMap) => {
if (booking.status !== ROOM_STATUS_CHECKEDIN) return null;

if (today === booking.checkout) {
if (!isFlat && nowTime > CHECKOUT_DEADLINE && booking.nights >= 1) {
return {
late_fee: calcLateCheckoutFee(booking.roomtype, nowTime <= LATE_CHECKOUT_HALF)
};
}
return null;
}

if (today < booking.checkout) {
if (!transaction) return null;
const nights = await calculateNights(booking.checkin, today);
const newAmount = roomCharge(isFlat ? 'nac' : booking.roomtype) * nights;
const paid =
transaction.status === STATUS_PAYMENT_COMPLETED ||
transaction.status === STATUS_CASH_COMPLETED;
const refund = paid
? Number(transaction.amount) + Number(transaction.discount)
: Number(transaction.discount);
const credit = refund - newAmount;
if (credit <= 0) return null;

const payerCardno = transaction.cardno || booking.bookedBy || booking.cardno;
const isSelfBooked = !payerCardno || payerCardno === booking.cardno;
const payerName = !isSelfBooked
? (bookerNameMap?.get(payerCardno) || null)
: null;

return {
credit,
is_self_booked: isSelfBooked,
payer_name: payerName
};
}

return null;
};

export const fetchKioskBookingsByCard = async (req, res) => {
const cardno = req.params.cardno?.trim();
if (!cardno) {
throw new ApiError(400, 'Card number is required');
}

req.log.info('fetch_kiosk_bookings_by_card_start', { cardno });

const nowIST = moment().tz('Asia/Kolkata');
const today = nowIST.format('YYYY-MM-DD');
const nowTime = nowIST.format('HH:mm:ss');
const kioskLookbackCutoff = moment(today).subtract(KIOSK_CHECKEDIN_LOOKBACK_DAYS, 'days').format('YYYY-MM-DD');
const activeStayWhere = {
cardno,
[Sequelize.Op.or]: [
{
status: ROOM_STATUS_CHECKEDIN,
checkout: { [Sequelize.Op.gte]: kioskLookbackCutoff }
},
{
status: ROOM_STATUS_PENDING_CHECKIN,
checkout: { [Sequelize.Op.gte]: today }
},
{
status: STATUS_PAYMENT_PENDING,
checkout: { [Sequelize.Op.gte]: today }
}
]
};

const bookingAttributes = [
'bookingid',
'cardno',
'bookedBy',
'checkin',
'checkout',
'nights',
'status'
];

// Batch 1: Query CardDb, RoomBooking, and FlatBooking in parallel
const [cardDetails, roomBookings, flatBookings] = await Promise.all([
CardDb.findOne({
where: { cardno },
attributes: ['cardno', 'issuedto']
}),
RoomBooking.findAll({
where: activeStayWhere,
attributes: [...bookingAttributes, 'roomno', 'roomtype'],
order: [['checkin', 'ASC']]
}),
FlatBooking.findAll({
where: activeStayWhere,
attributes: [...bookingAttributes, 'flatno'],
order: [['checkin', 'ASC']]
})
]);

if (!cardDetails) {
throw new ApiError(404, 'Card not found');
}

const allBookings = [...roomBookings, ...flatBookings];

const earlyCheckoutBookingIds = allBookings
.filter((b) => b.status === ROOM_STATUS_CHECKEDIN && today < b.checkout)
.map((b) => b.bookingid);

// Batch 2: Fetch transactions for early checkout stays first (if any)
const transactions = earlyCheckoutBookingIds.length
? await Transactions.findAll({
where: {
bookingid: earlyCheckoutBookingIds
}
})
: [];

const txMap = new Map(transactions.map((t) => [t.bookingid, t]));

// Collect all distinct booker and payer cards to resolve names in one single query
const relevantCardnos = [
...allBookings.map((b) => b.bookedBy),
...transactions.map((t) => t.cardno)
].filter((c) => c && c !== cardno);

const uniqueCardnos = [...new Set(relevantCardnos)];

const bookerCards = uniqueCardnos.length
? await CardDb.findAll({
where: { cardno: uniqueCardnos },
attributes: ['cardno', 'issuedto']
})
: [];

const bookerNameMap = new Map(bookerCards.map((c) => [c.cardno, c.issuedto]));

const withPreview = (isFlat) => async (booking) => {
const { bookedBy, ...publicFields } = booking.get({ plain: true });
const bookerName =
bookedBy && bookedBy !== cardno
? bookerNameMap.get(bookedBy) || null
: null;
const tx = txMap.get(publicFields.bookingid) || null;
return {
...publicFields,
booker_name: bookerName,
checkout_preview: await kioskCheckoutPreview(booking, isFlat, today, nowTime, tx, bookerNameMap)
};
};

const [roomBookingsOut, flatBookingsOut] = await Promise.all([
Promise.all(roomBookings.map(withPreview(false))),
Promise.all(flatBookings.map(withPreview(true)))
]);

req.log.info('fetch_kiosk_bookings_by_card_success', {
cardno,
roomBookingsCount: roomBookings.length,
flatBookingsCount: flatBookings.length
});

return res.status(200).send({
message: 'Fetched bookings',
data: {
room_booking: roomBookingsOut,
flat_booking: flatBookingsOut,
card_details: cardDetails
}
});
};

export const fetchRoomBookingsByCard = async (req, res) => {
const cardno = req.params.cardno;
req.log.info('fetch_room_bookings_by_card_start', { cardno });
Expand Down Expand Up @@ -2329,6 +2505,109 @@ export const fetchLateCheckoutFees = async (req, res) => {
const { payment_type } = req.query;
req.log.info('fetch_late_checkout_fees_start', { payment_type });

if (payment_type === 'overdue_checkouts') {
const nowIST = moment().tz('Asia/Kolkata');
const today = nowIST.format('YYYY-MM-DD');
const nowTime = nowIST.format('HH:mm:ss');
const isPastDeadline = nowTime > CHECKOUT_DEADLINE;

// Overdue condition:
// Past checkout date is always overdue.
// If today is checkout date and past 11:00:00 AM, it's overdue today.
const overdueCondition = isPastDeadline
? { checkout: { [Sequelize.Op.lte]: today } }
: { checkout: { [Sequelize.Op.lt]: today } };

const [overdueRooms, overdueFlats] = await Promise.all([
RoomBooking.findAll({
where: {
status: ROOM_STATUS_CHECKEDIN,
...overdueCondition
},
include: [
{
model: CardDb,
attributes: ['cardno', 'issuedto', 'mobno']
}
],
order: [['checkout', 'ASC']]
}),
FlatBooking.findAll({
where: {
status: ROOM_STATUS_CHECKEDIN,
...overdueCondition
},
include: [
{
model: CardDb,
attributes: ['cardno', 'issuedto', 'mobno']
}
],
order: [['checkout', 'ASC']]
})
]);

const formattedRooms = overdueRooms.map((rb) => {
const isToday = rb.checkout === today;
const isHalfDay = nowTime <= LATE_CHECKOUT_HALF;
// Day visits (nights < 1) are never charged a late fee per handleSameDayCheckout
const estLateFee = (isToday && rb.nights >= 1) ? calcLateCheckoutFee(rb.roomtype, isHalfDay) : null;
const daysOverdue = moment(today).diff(moment(rb.checkout), 'days');

return {
id: null,
amount: estLateFee,
est_late_fee: estLateFee,
days_overdue: Math.max(0, daysOverdue),
bookingid: rb.bookingid,
roomno: rb.roomno || '—',
roomtype: rb.roomtype || '—',
checkin: rb.checkin,
checkout: rb.checkout,
nights: rb.nights,
guest_name: rb.CardDb?.issuedto || '—',
mobile: rb.CardDb?.mobno || '—',
is_flat: false,
is_overdue: true
};
});

const formattedFlats = overdueFlats.map((fb) => {
const daysOverdue = moment(today).diff(moment(fb.checkout), 'days');

return {
id: null,
amount: null,
est_late_fee: null,
days_overdue: Math.max(0, daysOverdue),
bookingid: fb.bookingid,
roomno: fb.flatno ? `Flat ${fb.flatno}` : 'Flat',
roomtype: 'Flat',
checkin: fb.checkin,
checkout: fb.checkout,
nights: fb.nights,
guest_name: fb.CardDb?.issuedto || '—',
mobile: fb.CardDb?.mobno || '—',
is_flat: true,
is_overdue: true
};
});

const combined = [...formattedRooms, ...formattedFlats].sort((a, b) =>
String(a.checkout).localeCompare(String(b.checkout))
);

req.log.info('fetch_overdue_checkouts_success', { count: combined.length });
return res.status(200).json({
success: true,
data: combined,
meta: {
count: combined.length,
payment_type
}
});
}

const statusMap = {
payment_pending: ['cash pending'],
payment_done: ['completed'],
Expand Down
Loading
Loading