mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
fix(trips): count trip dates in calendar days, the same on REST and MCP
Filling in a missing end date and keeping a trip's length when its start moved both shifted a local-time Date, so a trip crossing a DST change lost or gained a day. Both now use addIsoDays. The one-date-makes-a-week default moves from the REST controller into TripsService.create, so the MCP create_trip tool no longer stores a half-dated trip, and the trip and its days are written in one transaction.
This commit is contained in:
@@ -50,9 +50,6 @@ export const TRIP_COVER_FILE_FILTER: Options['fileFilter'] = (_req, file, cb) =>
|
||||
else cb(new Error('Only jpg, png, gif, webp images allowed'));
|
||||
};
|
||||
|
||||
const toDateStr = (d: Date) => d.toISOString().slice(0, 10);
|
||||
const addDays = (d: Date, n: number) => { const r = new Date(d); r.setDate(r.getDate() + n); return r; };
|
||||
|
||||
/**
|
||||
* /api/trips — the trip aggregate root.
|
||||
*
|
||||
@@ -115,10 +112,8 @@ export class TripsController {
|
||||
}
|
||||
// Presence/shape validation happens in the ZodValidationPipe (tripCreateRequestSchema).
|
||||
const { title, description, currency, reminder_days, day_count } = body;
|
||||
let start_date: string | null = body.start_date || null;
|
||||
let end_date: string | null = body.end_date || null;
|
||||
if (start_date && !end_date) end_date = toDateStr(addDays(new Date(start_date), 6));
|
||||
else if (!start_date && end_date) start_date = toDateStr(addDays(new Date(end_date), -6));
|
||||
const start_date: string | null = body.start_date || null;
|
||||
const end_date: string | null = body.end_date || null;
|
||||
if (start_date && end_date && new Date(end_date) < new Date(start_date)) {
|
||||
throw new HttpException({ error: 'End date must be after start date' }, 400);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import { TourWaypoints } from '../../db/entities/TourWaypoints.entity';
|
||||
import type { TourWaypointRow } from '../../db/repositories/TourWaypoints.repository';
|
||||
import {
|
||||
MAX_TRIP_DAYS,
|
||||
addIsoDays,
|
||||
planDayGrid,
|
||||
resolveDayGridRange,
|
||||
tripSpanDays,
|
||||
@@ -405,23 +406,34 @@ export class TripsService {
|
||||
}
|
||||
|
||||
async create(userId: number, data: CreateTripData) {
|
||||
if (data.start_date && data.end_date) assertTripSpan(data.start_date, data.end_date);
|
||||
// One given date makes a week, on REST and MCP alike. Calendar arithmetic
|
||||
// in UTC: a local-time Date shifted across a DST change lost or gained a day.
|
||||
let startDate = data.start_date || null;
|
||||
let endDate = data.end_date || null;
|
||||
if (startDate && !endDate) endDate = addIsoDays(startDate, 6);
|
||||
else if (!startDate && endDate) startDate = addIsoDays(endDate, -6);
|
||||
if (startDate && endDate) assertTripSpan(startDate, endDate);
|
||||
const rd = data.reminder_days !== undefined
|
||||
? (Number(data.reminder_days) >= 0 && Number(data.reminder_days) <= 30 ? Number(data.reminder_days) : 3)
|
||||
: 3;
|
||||
const currency = data.currency || (await this.defaultCurrencyFor(userId));
|
||||
|
||||
const tripId = await this.tripsRepo.insertTrip({ // TP18
|
||||
user_id: userId,
|
||||
title: data.title,
|
||||
description: data.description || null,
|
||||
start_date: data.start_date || null,
|
||||
end_date: data.end_date || null,
|
||||
currency: data.currency || (await this.defaultCurrencyFor(userId)),
|
||||
reminder_days: rd,
|
||||
// The trip and its days commit together: a failure in between used to leave
|
||||
// a dated trip without a single day.
|
||||
const tripId = await this.uow.transactional(async () => {
|
||||
const id = await this.tripsRepo.insertTrip({ // TP18
|
||||
user_id: userId,
|
||||
title: data.title,
|
||||
description: data.description || null,
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
currency,
|
||||
reminder_days: rd,
|
||||
});
|
||||
await this.generateDays(id, startDate, endDate, data.day_count);
|
||||
return id;
|
||||
});
|
||||
|
||||
await this.generateDays(tripId, data.start_date || null, data.end_date || null, data.day_count);
|
||||
|
||||
const trip = await this.tripsRepo.findForViewer(tripId, userId); // TP19 — the creator always owns it, so the access predicate is trivially satisfied
|
||||
return { trip, tripId, reminderDays: rd };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user