mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-22 06:56:35 +00:00
fix(vacay): enforce weekend blocking in toggleEntry service
With block_weekends on, toggling a weekend day via MCP or the plugin RPC
still added the entry and burned allowance — the guard lived only in the
web UI (mcp-smoke-test I-02). toggleEntry now rejects inserts and
in-place conversions on blocked days with { error: 'weekend_blocked' }
(the votePoll pattern); removing an existing entry stays allowed so
stray weekend entries remain cleanable. The controller maps the code to
a 400, the MCP tool to an isError result.
This commit is contained in:
@@ -229,7 +229,11 @@ export class VacayController {
|
||||
}
|
||||
userId = tid;
|
||||
}
|
||||
return this.vacay.toggleEntry(userId, planId, body.date, body.fraction, body.kind, socketId);
|
||||
const result = this.vacay.toggleEntry(userId, planId, body.date, body.fraction, body.kind, socketId);
|
||||
if (result.error === 'weekend_blocked') {
|
||||
throw new HttpException({ error: 'Weekend days are blocked on this plan' }, 400);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Post('entries/company-holiday')
|
||||
|
||||
@@ -272,6 +272,7 @@ export class VacayMcp {
|
||||
if (this.auth.isDemoUser(ctx.userId)) return demoDenied();
|
||||
const planId = this.vacay.getActivePlanId(ctx.userId);
|
||||
const result = this.vacay.toggleEntry(ctx.userId, planId, date, 1, 'vacation', undefined);
|
||||
if (result.error) return errorResult(result.error);
|
||||
return ok(result);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ export interface VacayPlan {
|
||||
school_holidays_enabled: number;
|
||||
company_holidays_enabled: number;
|
||||
carry_over_enabled: number;
|
||||
weekend_days: string | null;
|
||||
week_start: number | null;
|
||||
}
|
||||
|
||||
export interface VacayUserYear {
|
||||
@@ -120,6 +122,16 @@ function normalizeKind(value: unknown): 'vacation' | 'comp' {
|
||||
return value === 'comp' ? 'comp' : 'vacation';
|
||||
}
|
||||
|
||||
/**
|
||||
* Same semantics as the client's isWeekend guard (holidays.ts): UTC weekday,
|
||||
* weekend_days falls back to Sat/Sun when the column is NULL or empty.
|
||||
*/
|
||||
function isBlockedWeekend(plan: Pick<VacayPlan, 'block_weekends' | 'weekend_days'>, date: string): boolean {
|
||||
if (!plan.block_weekends) return false;
|
||||
const days = plan.weekend_days ? String(plan.weekend_days).split(',').map(Number) : [0, 6];
|
||||
return days.includes(new Date(date + 'T00:00:00Z').getUTCDay());
|
||||
}
|
||||
|
||||
const pad2 = (n: number) => String(n).padStart(2, '0');
|
||||
|
||||
/** Days a month has in every year — February caps at 28 so no boundary lands on a date a common year lacks. */
|
||||
@@ -1064,9 +1076,11 @@ export class VacayService {
|
||||
return { entries, companyHolidays };
|
||||
}
|
||||
|
||||
toggleEntry(userId: number, planId: number, date: string, fraction?: unknown, kind?: unknown, socketId?: string): { action: string; fraction?: number; kind?: string } {
|
||||
toggleEntry(userId: number, planId: number, date: string, fraction?: unknown, kind?: unknown, socketId?: string): { action?: string; fraction?: number; kind?: string; error?: string } {
|
||||
const frac = normalizeFraction(fraction);
|
||||
const knd = normalizeKind(kind);
|
||||
const plan = this.db.get<VacayPlan>('SELECT * FROM vacay_plans WHERE id = ?', planId);
|
||||
const weekendBlocked = plan ? isBlockedWeekend(plan, date) : false;
|
||||
const existing = this.db.get<{ id: number; fraction: number; kind: string | null }>('SELECT id, fraction, kind FROM vacay_entries WHERE user_id = ? AND date = ? AND plan_id = ?', userId, date, planId);
|
||||
if (existing) {
|
||||
// Clicking the exact same day again (same type AND same fraction) clears it;
|
||||
@@ -1076,10 +1090,14 @@ export class VacayService {
|
||||
this.notifyPlanUsers(planId, socketId);
|
||||
return { action: 'removed' };
|
||||
}
|
||||
// Removing a stray entry on a blocked day stays possible; keeping one
|
||||
// there (converted in place) does not.
|
||||
if (weekendBlocked) return { error: 'weekend_blocked' };
|
||||
this.db.run('UPDATE vacay_entries SET fraction = ?, kind = ? WHERE id = ?', frac, knd, existing.id);
|
||||
this.notifyPlanUsers(planId, socketId);
|
||||
return { action: 'updated', fraction: frac, kind: knd };
|
||||
}
|
||||
if (weekendBlocked) return { error: 'weekend_blocked' };
|
||||
this.db.run('INSERT INTO vacay_entries (plan_id, user_id, date, note, fraction, kind) VALUES (?, ?, ?, ?, ?, ?)', planId, userId, date, '', frac, knd);
|
||||
this.notifyPlanUsers(planId, socketId);
|
||||
return { action: 'added', fraction: frac, kind: knd };
|
||||
|
||||
Reference in New Issue
Block a user