import { db } from '../db/client.js'; import type { BirdGender, BirdMilestoneReminderCandidateRow, BirdMilestoneReminderDeliveryRow, BirdMilestoneReminderType, BirdRow, LostBirdMatchRow, MedicationAdministrationRow, MedicationDoseScheduleItem, MedicationRow, PendingBirdTransferRow, VetVisitRow, WeightRow, } from '../types.js'; const birdSelectFields = ` birds.id, birds.workspace_id, birds.name, birds.tag_id, birds.species, birds.gender, birds.date_of_birth::text, birds.gotcha_day::text, birds.chart_color, birds.photo_data_url, birds.notify_on_dob, birds.notify_on_gotcha_day, birds.memorialized_at, birds.memorialized_on::text, birds.memorial_note, birds.notify_on_memorial_day, birds.created_at, latest.weight_grams AS latest_weight_grams, latest.recorded_on::text AS latest_recorded_on `; export const getBirdById = async (birdId: string, workspaceId: number) => { const result = await db.query( `SELECT ${birdSelectFields} FROM birds LEFT JOIN LATERAL ( SELECT weight_grams, recorded_on FROM weight_records WHERE weight_records.bird_id = birds.id ORDER BY recorded_on DESC LIMIT 1 ) latest ON TRUE WHERE birds.id = $1 AND birds.workspace_id = $2`, [birdId, workspaceId], ); return result.rows[0] ?? null; }; export const listBirds = async (workspaceId: number) => { const result = await db.query( `SELECT ${birdSelectFields} FROM birds LEFT JOIN LATERAL ( SELECT weight_grams, recorded_on FROM weight_records WHERE weight_records.bird_id = birds.id ORDER BY recorded_on DESC LIMIT 1 ) latest ON TRUE WHERE birds.workspace_id = $1 AND birds.memorialized_at IS NULL ORDER BY birds.name ASC`, [workspaceId], ); return result.rows; }; export const listMemorializedBirds = async (workspaceId: number) => { const result = await db.query( `SELECT ${birdSelectFields} FROM birds LEFT JOIN LATERAL ( SELECT weight_grams, recorded_on FROM weight_records WHERE weight_records.bird_id = birds.id ORDER BY recorded_on DESC LIMIT 1 ) latest ON TRUE WHERE birds.workspace_id = $1 AND birds.memorialized_at IS NOT NULL ORDER BY birds.memorialized_on DESC NULLS LAST, birds.name ASC`, [workspaceId], ); return result.rows; }; export const findBirdsByBandId = async (tagId: string) => { const result = await db.query( `SELECT ${birdSelectFields}, workspaces.name AS workspace_name, workspaces.billing_email AS workspace_billing_email FROM birds INNER JOIN workspaces ON workspaces.id = birds.workspace_id LEFT JOIN LATERAL ( SELECT weight_grams, recorded_on FROM weight_records WHERE weight_records.bird_id = birds.id ORDER BY recorded_on DESC LIMIT 1 ) latest ON TRUE WHERE birds.tag_id IS NOT NULL AND BTRIM(birds.tag_id) <> '' AND LOWER(BTRIM(birds.tag_id)) NOT IN ('unknown', 'not recorded', 'n/a', 'na', 'none') AND LOWER(birds.tag_id) = LOWER($1) AND birds.memorialized_at IS NULL ORDER BY birds.created_at ASC LIMIT 10`, [tagId], ); return result.rows; }; export const listDueBirdMilestoneReminders = async (runDate: string) => { const result = await db.query( `WITH reminder_context AS ( SELECT $1::date AS run_date, EXTRACT(YEAR FROM $1::date)::int AS reminder_year ) SELECT ${birdSelectFields}, workspaces.name AS workspace_name, 'hatch_day'::text AS reminder_type, birds.date_of_birth::text AS reminder_date, reminder_context.reminder_year FROM birds INNER JOIN workspaces ON workspaces.id = birds.workspace_id CROSS JOIN reminder_context LEFT JOIN LATERAL ( SELECT weight_grams, recorded_on FROM weight_records WHERE weight_records.bird_id = birds.id ORDER BY recorded_on DESC LIMIT 1 ) latest ON TRUE WHERE birds.notify_on_dob = TRUE AND birds.memorialized_at IS NULL AND birds.date_of_birth IS NOT NULL AND EXTRACT(MONTH FROM birds.date_of_birth) = EXTRACT(MONTH FROM reminder_context.run_date) AND EXTRACT(DAY FROM birds.date_of_birth) = EXTRACT(DAY FROM reminder_context.run_date) AND NOT EXISTS ( SELECT 1 FROM bird_milestone_reminder_deliveries deliveries WHERE deliveries.bird_id = birds.id AND deliveries.reminder_type = 'hatch_day' AND deliveries.reminder_year = reminder_context.reminder_year ) UNION ALL SELECT ${birdSelectFields}, workspaces.name AS workspace_name, 'gotcha_day'::text AS reminder_type, birds.gotcha_day::text AS reminder_date, reminder_context.reminder_year FROM birds INNER JOIN workspaces ON workspaces.id = birds.workspace_id CROSS JOIN reminder_context LEFT JOIN LATERAL ( SELECT weight_grams, recorded_on FROM weight_records WHERE weight_records.bird_id = birds.id ORDER BY recorded_on DESC LIMIT 1 ) latest ON TRUE WHERE birds.notify_on_gotcha_day = TRUE AND birds.memorialized_at IS NULL AND birds.gotcha_day IS NOT NULL AND EXTRACT(MONTH FROM birds.gotcha_day) = EXTRACT(MONTH FROM reminder_context.run_date) AND EXTRACT(DAY FROM birds.gotcha_day) = EXTRACT(DAY FROM reminder_context.run_date) AND NOT EXISTS ( SELECT 1 FROM bird_milestone_reminder_deliveries deliveries WHERE deliveries.bird_id = birds.id AND deliveries.reminder_type = 'gotcha_day' AND deliveries.reminder_year = reminder_context.reminder_year ) UNION ALL SELECT ${birdSelectFields}, workspaces.name AS workspace_name, 'memorial_day'::text AS reminder_type, birds.memorialized_on::text AS reminder_date, reminder_context.reminder_year FROM birds INNER JOIN workspaces ON workspaces.id = birds.workspace_id CROSS JOIN reminder_context LEFT JOIN LATERAL ( SELECT weight_grams, recorded_on FROM weight_records WHERE weight_records.bird_id = birds.id ORDER BY recorded_on DESC LIMIT 1 ) latest ON TRUE WHERE birds.notify_on_memorial_day = TRUE AND birds.memorialized_at IS NOT NULL AND birds.memorialized_on IS NOT NULL AND EXTRACT(MONTH FROM birds.memorialized_on) = EXTRACT(MONTH FROM reminder_context.run_date) AND EXTRACT(DAY FROM birds.memorialized_on) = EXTRACT(DAY FROM reminder_context.run_date) AND NOT EXISTS ( SELECT 1 FROM bird_milestone_reminder_deliveries deliveries WHERE deliveries.bird_id = birds.id AND deliveries.reminder_type = 'memorial_day' AND deliveries.reminder_year = reminder_context.reminder_year ) ORDER BY workspace_name ASC, name ASC, reminder_type ASC`, [runDate], ); return result.rows; }; export const createBirdMilestoneReminderDelivery = async ({ birdId, workspaceId, reminderType, reminderYear, deliveredOn, }: { birdId: string; workspaceId: number; reminderType: BirdMilestoneReminderType; reminderYear: number; deliveredOn: string; }) => { const result = await db.query( `INSERT INTO bird_milestone_reminder_deliveries (bird_id, workspace_id, reminder_type, reminder_year, delivered_on) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (bird_id, reminder_type, reminder_year) DO NOTHING RETURNING id, bird_id, workspace_id, reminder_type, reminder_year, delivered_on::text, created_at`, [birdId, workspaceId, reminderType, reminderYear, deliveredOn], ); return result.rows[0] ?? null; }; export const createBird = async ({ workspaceId, name, tagId, species, gender, dateOfBirth, gotchaDay, chartColor, photoDataUrl, notifyOnDob, notifyOnGotchaDay, }: { workspaceId: number; name: string; tagId: string | null; species: string; gender: BirdGender; dateOfBirth: string | null; gotchaDay: string | null; chartColor: string; photoDataUrl: string | null; notifyOnDob: boolean; notifyOnGotchaDay: boolean; }) => { const result = await db.query( `INSERT INTO birds (workspace_id, name, tag_id, species, gender, date_of_birth, gotcha_day, chart_color, photo_data_url, notify_on_dob, notify_on_gotcha_day) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id, workspace_id, name, tag_id, species, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, notify_on_dob, notify_on_gotcha_day, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at, NULL::text AS latest_weight_grams, NULL::text AS latest_recorded_on`, [workspaceId, name, tagId, species, gender, dateOfBirth, gotchaDay, chartColor, photoDataUrl, notifyOnDob, notifyOnGotchaDay], ); return result.rows[0] ?? null; }; export const updateBird = async ({ birdId, workspaceId, name, tagId, species, gender, dateOfBirth, gotchaDay, chartColor, photoDataUrl, notifyOnDob, notifyOnGotchaDay, }: { birdId: string; workspaceId: number; name: string; tagId: string | null; species: string; gender: BirdGender; dateOfBirth: string | null; gotchaDay: string | null; chartColor: string; photoDataUrl: string | null; notifyOnDob: boolean; notifyOnGotchaDay: boolean; }) => { const result = await db.query( `UPDATE birds SET name = $2, tag_id = $3, species = $4, gender = $5, date_of_birth = $6, gotcha_day = $7, chart_color = $8, photo_data_url = $9, notify_on_dob = $10, notify_on_gotcha_day = $11 WHERE id = $1 AND workspace_id = $12 AND memorialized_at IS NULL RETURNING id, workspace_id, name, tag_id, species, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, notify_on_dob, notify_on_gotcha_day, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at, ( SELECT weight_grams::text FROM weight_records WHERE bird_id = birds.id ORDER BY recorded_on DESC LIMIT 1 ) AS latest_weight_grams, ( SELECT recorded_on::text FROM weight_records WHERE bird_id = birds.id ORDER BY recorded_on DESC LIMIT 1 ) AS latest_recorded_on`, [birdId, name, tagId, species, gender, dateOfBirth, gotchaDay, chartColor, photoDataUrl, notifyOnDob, notifyOnGotchaDay, workspaceId], ); return result.rows[0] ?? null; }; export const memorializeBird = async ({ birdId, workspaceId, memorializedOn, memorialNote, notifyOnMemorialDay, }: { birdId: string; workspaceId: number; memorializedOn: string; memorialNote: string | null; notifyOnMemorialDay: boolean; }) => { const result = await db.query( `UPDATE birds SET memorialized_at = CURRENT_TIMESTAMP, memorialized_on = $3, memorial_note = $4, notify_on_memorial_day = $5 WHERE id = $1 AND workspace_id = $2 AND memorialized_at IS NULL RETURNING id, workspace_id, name, tag_id, species, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, notify_on_dob, notify_on_gotcha_day, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at, ( SELECT weight_grams::text FROM weight_records WHERE bird_id = birds.id ORDER BY recorded_on DESC LIMIT 1 ) AS latest_weight_grams, ( SELECT recorded_on::text FROM weight_records WHERE bird_id = birds.id ORDER BY recorded_on DESC LIMIT 1 ) AS latest_recorded_on`, [birdId, workspaceId, memorializedOn, memorialNote, notifyOnMemorialDay], ); return result.rows[0] ?? null; }; export const updateMemorialReminderPreference = async ({ birdId, workspaceId, notifyOnMemorialDay, }: { birdId: string; workspaceId: number; notifyOnMemorialDay: boolean; }) => { const result = await db.query( `UPDATE birds SET notify_on_memorial_day = $3 WHERE id = $1 AND workspace_id = $2 AND memorialized_at IS NOT NULL RETURNING id, workspace_id, name, tag_id, species, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, notify_on_dob, notify_on_gotcha_day, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at, ( SELECT weight_grams::text FROM weight_records WHERE bird_id = birds.id ORDER BY recorded_on DESC LIMIT 1 ) AS latest_weight_grams, ( SELECT recorded_on::text FROM weight_records WHERE bird_id = birds.id ORDER BY recorded_on DESC LIMIT 1 ) AS latest_recorded_on`, [birdId, workspaceId, notifyOnMemorialDay], ); return result.rows[0] ?? null; }; export const deleteBird = async (birdId: string, workspaceId: number) => { const result = await db.query<{ id: string }>( `DELETE FROM birds WHERE id = $1 AND workspace_id = $2 RETURNING id`, [birdId, workspaceId], ); return Boolean(result.rowCount); }; export const transferBirdToWorkspace = async (birdId: string, sourceWorkspaceId: number, targetWorkspaceId: number) => { const result = await db.query( `UPDATE birds SET workspace_id = $3 WHERE id = $1 AND workspace_id = $2 AND memorialized_at IS NULL RETURNING id, workspace_id, name, tag_id, species, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, notify_on_dob, notify_on_gotcha_day, memorialized_at, memorialized_on::text, memorial_note, notify_on_memorial_day, created_at, ( SELECT weight_grams::text FROM weight_records WHERE bird_id = birds.id ORDER BY recorded_on DESC LIMIT 1 ) AS latest_weight_grams, ( SELECT recorded_on::text FROM weight_records WHERE bird_id = birds.id ORDER BY recorded_on DESC LIMIT 1 ) AS latest_recorded_on`, [birdId, sourceWorkspaceId, targetWorkspaceId], ); return result.rows[0] ?? null; }; export const createPendingBirdTransfer = async ({ birdId, sourceWorkspaceId, destinationOwnerEmail, requestedByUserId, }: { birdId: string; sourceWorkspaceId: number; destinationOwnerEmail: string; requestedByUserId: string; }) => { const result = await db.query( `INSERT INTO pending_bird_transfers (bird_id, source_workspace_id, destination_owner_email, requested_by_user_id) VALUES ($1, $2, $3, $4) ON CONFLICT (bird_id) WHERE completed_at IS NULL DO UPDATE SET destination_owner_email = EXCLUDED.destination_owner_email, requested_by_user_id = EXCLUDED.requested_by_user_id, last_error = NULL, created_at = CURRENT_TIMESTAMP RETURNING id, bird_id, source_workspace_id, destination_owner_email, requested_by_user_id, completed_at::text, completed_workspace_id, last_error, created_at`, [birdId, sourceWorkspaceId, destinationOwnerEmail, requestedByUserId], ); return result.rows[0] ?? null; }; export const listPendingBirdTransfersForOwnerEmail = async (ownerEmail: string) => { const result = await db.query( `SELECT id, bird_id, source_workspace_id, destination_owner_email, requested_by_user_id, completed_at::text, completed_workspace_id, last_error, created_at FROM pending_bird_transfers WHERE LOWER(destination_owner_email) = LOWER($1) AND completed_at IS NULL ORDER BY created_at ASC`, [ownerEmail], ); return result.rows; }; export const markPendingBirdTransferCompleted = async (transferId: string, completedWorkspaceId: number) => { await db.query( `UPDATE pending_bird_transfers SET completed_at = CURRENT_TIMESTAMP, completed_workspace_id = $2, last_error = NULL WHERE id = $1`, [transferId, completedWorkspaceId], ); }; export const markPendingBirdTransferFailed = async (transferId: string, lastError: string) => { await db.query( `UPDATE pending_bird_transfers SET last_error = $2 WHERE id = $1`, [transferId, lastError], ); }; export const completePendingBirdTransfersForOwner = async (ownerEmail: string, targetWorkspaceId: number) => { const transfers = await listPendingBirdTransfersForOwnerEmail(ownerEmail); let completed = 0; let failed = 0; for (const transfer of transfers) { try { const bird = await transferBirdToWorkspace(transfer.bird_id, transfer.source_workspace_id, targetWorkspaceId); if (!bird) { failed += 1; await markPendingBirdTransferFailed(transfer.id, 'Bird is no longer available in the source flock.'); continue; } await markPendingBirdTransferCompleted(transfer.id, targetWorkspaceId); completed += 1; } catch (error) { failed += 1; const message = typeof error === 'object' && error && 'code' in error && error.code === '23505' ? 'The receiving flock already has a bird using the same band/tag ID.' : error instanceof Error ? error.message : 'Unable to complete pending bird transfer.'; await markPendingBirdTransferFailed(transfer.id, message); } } return { completed, failed }; }; export const listWeightsForBird = async (birdId: string, workspaceId: number, days: number) => { const result = await db.query( `SELECT id, bird_id, weight_grams, recorded_on::text, notes FROM weight_records WHERE bird_id = $1 AND EXISTS ( SELECT 1 FROM birds WHERE birds.id = weight_records.bird_id AND birds.workspace_id = $3 ) AND recorded_on >= CURRENT_DATE - (($2::int - 1) * INTERVAL '1 day') ORDER BY recorded_on ASC`, [birdId, days, workspaceId], ); return result.rows; }; export const createWeightForBird = async (birdId: string, weightGrams: number, recordedOn: string, notes: string | null) => { const result = await db.query( `INSERT INTO weight_records (bird_id, weight_grams, recorded_on, notes) VALUES ($1, $2, $3, $4) RETURNING id, bird_id, weight_grams, recorded_on::text, notes`, [birdId, weightGrams, recordedOn, notes], ); return result.rows[0] ?? null; }; export const listVetVisitsForBird = async (birdId: string, workspaceId: number) => { const result = await db.query( `SELECT id, bird_id, visited_on::text, clinic_name, reason, notes FROM vet_visits WHERE bird_id = $1 AND EXISTS ( SELECT 1 FROM birds WHERE birds.id = vet_visits.bird_id AND birds.workspace_id = $2 ) ORDER BY visited_on DESC, created_at DESC`, [birdId, workspaceId], ); return result.rows; }; export const createVetVisitForBird = async (birdId: string, visitedOn: string, clinicName: string, reason: string, notes: string | null) => { const result = await db.query( `INSERT INTO vet_visits (bird_id, visited_on, clinic_name, reason, notes) VALUES ($1, $2, $3, $4, $5) RETURNING id, bird_id, visited_on::text, clinic_name, reason, notes`, [birdId, visitedOn, clinicName, reason, notes], ); return result.rows[0] ?? null; }; export const updateVetVisitForBird = async ( visitId: string, birdId: string, visitedOn: string, clinicName: string, reason: string, notes: string | null, ) => { const result = await db.query( `UPDATE vet_visits SET visited_on = $3, clinic_name = $4, reason = $5, notes = $6 WHERE id = $1 AND bird_id = $2 RETURNING id, bird_id, visited_on::text, clinic_name, reason, notes`, [visitId, birdId, visitedOn, clinicName, reason, notes], ); return result.rows[0] ?? null; }; export const deleteVetVisitForBird = async (visitId: string, birdId: string) => { const result = await db.query<{ id: string }>( `DELETE FROM vet_visits WHERE id = $1 AND bird_id = $2 RETURNING id`, [visitId, birdId], ); return (result.rowCount ?? 0) > 0; }; export const listMedicationsForBird = async (birdId: string, workspaceId: number) => { const result = await db.query( `SELECT id, bird_id, name, dosage, frequency, dose_schedule, route, start_date::text, end_date::text, notes FROM medications WHERE bird_id = $1 AND EXISTS ( SELECT 1 FROM birds WHERE birds.id = medications.bird_id AND birds.workspace_id = $2 ) ORDER BY COALESCE(end_date, '9999-12-31'::date) DESC, start_date DESC, created_at DESC`, [birdId, workspaceId], ); return result.rows; }; export const createMedicationForBird = async ( birdId: string, name: string, dosage: string, frequency: string, doseSchedule: MedicationDoseScheduleItem[], route: string | null, startDate: string, endDate: string | null, notes: string | null, ) => { const result = await db.query( `INSERT INTO medications (bird_id, name, dosage, frequency, dose_schedule, route, start_date, end_date, notes) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id, bird_id, name, dosage, frequency, dose_schedule, route, start_date::text, end_date::text, notes`, [birdId, name, dosage, frequency, JSON.stringify(doseSchedule), route, startDate, endDate, notes], ); return result.rows[0] ?? null; }; export const updateMedicationForBird = async ( medicationId: string, birdId: string, name: string, dosage: string, frequency: string, doseSchedule: MedicationDoseScheduleItem[], route: string | null, startDate: string, endDate: string | null, notes: string | null, ) => { const result = await db.query( `UPDATE medications SET name = $3, dosage = $4, frequency = $5, dose_schedule = $6, route = $7, start_date = $8, end_date = $9, notes = $10 WHERE id = $1 AND bird_id = $2 RETURNING id, bird_id, name, dosage, frequency, dose_schedule, route, start_date::text, end_date::text, notes`, [medicationId, birdId, name, dosage, frequency, JSON.stringify(doseSchedule), route, startDate, endDate, notes], ); return result.rows[0] ?? null; }; export const deleteMedicationForBird = async (medicationId: string, birdId: string) => { const result = await db.query<{ id: string }>( `DELETE FROM medications WHERE id = $1 AND bird_id = $2 RETURNING id`, [medicationId, birdId], ); return (result.rowCount ?? 0) > 0; }; export const listMedicationAdministrationsForBird = async (birdId: string, workspaceId: number) => { const result = await db.query( `SELECT id, medication_id, bird_id, administered_on::text, administration_slot, status, notes, created_by_user_id, created_at FROM medication_administrations WHERE bird_id = $1 AND EXISTS ( SELECT 1 FROM birds WHERE birds.id = medication_administrations.bird_id AND birds.workspace_id = $2 ) ORDER BY administered_on DESC, created_at DESC`, [birdId, workspaceId], ); return result.rows; }; export const upsertMedicationAdministrationForBird = async ( medicationId: string, birdId: string, workspaceId: number, administeredOn: string, administrationSlot: string, status: 'administered' | 'missed', notes: string | null, createdByUserId: string | null, ) => { const result = await db.query( `INSERT INTO medication_administrations (medication_id, bird_id, administered_on, administration_slot, status, notes, created_by_user_id) SELECT $1, $2, $4, $5, $6, $7, $8 WHERE EXISTS ( SELECT 1 FROM medications JOIN birds ON birds.id = medications.bird_id WHERE medications.id = $1 AND medications.bird_id = $2 AND birds.workspace_id = $3 ) ON CONFLICT (medication_id, administered_on, administration_slot) DO UPDATE SET status = EXCLUDED.status, notes = EXCLUDED.notes, created_by_user_id = EXCLUDED.created_by_user_id, created_at = CURRENT_TIMESTAMP RETURNING id, medication_id, bird_id, administered_on::text, administration_slot, status, notes, created_by_user_id, created_at`, [medicationId, birdId, workspaceId, administeredOn, administrationSlot, status, notes, createdByUserId], ); return result.rows[0] ?? null; };