import { db } from '../db/client.js'; import type { BirdGender, BirdMilestoneReminderCandidateRow, BirdMilestoneReminderDeliveryRow, BirdMilestoneReminderType, BirdRow, BirdTimelineEventRow, BirdTimelineEventType, BirdTransferCodeRow, LostBirdMatchRow, MedicationAdministrationRow, MedicationDoseScheduleItem, MedicationReminderCandidateRow, MedicationReminderDeliveryRow, MedicationRow, PendingBirdTransferRow, VetVisitRow, WeightRow, } from '../types.js'; const birdSelectFields = ` birds.id, birds.workspace_id, birds.name, birds.tag_id, birds.species, birds.motivators, birds.demotivators, birds.favorite_snack, birds.location_label, birds.location_details, birds.vet_clinic_name, birds.vet_clinic_address, birds.vet_account_number, birds.vet_doctor_name, birds.gender, birds.date_of_birth::text, birds.gotcha_day::text, birds.chart_color, birds.photo_data_url, birds.photo_object_key, birds.photo_content_type, birds.photo_updated_at, birds.notify_on_dob, birds.notify_on_gotcha_day, birds.public_profile_code, birds.public_profile_enabled, 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 `; type WorkspaceTimelineSnapshot = { workspace_id: number; workspace_name: string; owner_email: string | null; }; const getWorkspaceTimelineSnapshot = async (workspaceId: number) => { const result = await db.query( `SELECT workspaces.id AS workspace_id, workspaces.name AS workspace_name, COALESCE(workspaces.billing_email, owner_member.invite_email, owner_member.email) AS owner_email FROM workspaces LEFT JOIN LATERAL ( SELECT invite_email, email FROM workspace_members WHERE workspace_members.workspace_id = workspaces.id AND workspace_members.role = 'owner' ORDER BY accepted_at DESC NULLS LAST, created_at ASC LIMIT 1 ) owner_member ON TRUE WHERE workspaces.id = $1`, [workspaceId], ); return result.rows[0] ?? null; }; 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 getBirdByPublicProfileCode = async (publicProfileCode: string) => { 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.public_profile_code = $1 AND birds.public_profile_enabled = TRUE AND birds.memorialized_at IS NULL`, [publicProfileCode], ); 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 createBirdTimelineEvent = async ({ birdId, eventType, fromWorkspaceId, toWorkspaceId, locationLabel, locationDetails, note, eventDate, createdByUserId, }: { birdId: string; eventType: BirdTimelineEventType; fromWorkspaceId?: number | null; toWorkspaceId?: number | null; locationLabel?: string | null; locationDetails?: Record | null; note?: string | null; eventDate?: string | null; createdByUserId?: string | null; }) => { const [fromWorkspace, toWorkspace] = await Promise.all([ fromWorkspaceId ? getWorkspaceTimelineSnapshot(fromWorkspaceId) : Promise.resolve(null), toWorkspaceId ? getWorkspaceTimelineSnapshot(toWorkspaceId) : Promise.resolve(null), ]); const result = await db.query( `INSERT INTO bird_timeline_events ( bird_id, event_type, from_workspace_id, to_workspace_id, from_workspace_name, to_workspace_name, from_owner_email, to_owner_email, location_label, note, event_date, created_by_user_id, location_details ) VALUES ( $1, $2, $3, $4, $5::varchar(160), $6::varchar(160), $7::varchar(320), $8::varchar(320), COALESCE($9::varchar(160), $6::varchar(160), $5::varchar(160)), $10, COALESCE($11::date, CURRENT_DATE), $12, $13 ) RETURNING id, bird_id, event_type, from_workspace_id, to_workspace_id, from_workspace_name, to_workspace_name, from_owner_email, to_owner_email, location_label, location_details, note, event_date::text, created_by_user_id, created_at`, [ birdId, eventType, fromWorkspaceId ?? null, toWorkspaceId ?? null, fromWorkspace?.workspace_name ?? null, toWorkspace?.workspace_name ?? null, fromWorkspace?.owner_email ?? null, toWorkspace?.owner_email ?? null, locationLabel ?? null, note ?? null, eventDate ?? null, createdByUserId ?? null, locationDetails ?? null, ], ); return result.rows[0] ?? null; }; export const listBirdTimelineEvents = async (birdId: string, workspaceId: number) => { const result = await db.query( `SELECT id, bird_id, event_type, from_workspace_id, to_workspace_id, from_workspace_name, to_workspace_name, from_owner_email, to_owner_email, location_label, location_details, note, event_date::text, created_by_user_id, created_at FROM bird_timeline_events WHERE bird_id = $1 AND EXISTS ( SELECT 1 FROM birds WHERE birds.id = bird_timeline_events.bird_id AND birds.workspace_id = $2 ) ORDER BY event_date DESC, created_at DESC`, [birdId, 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 listDueMedicationReminders = async (runDate: string, currentTime: string) => { const result = await db.query( `SELECT ${birdSelectFields}, workspaces.name AS workspace_name, medications.id AS medication_id, medications.name AS medication_name, medications.dosage, medications.frequency, medications.dose_schedule, medications.route, medications.start_date::text AS medication_start_date, medications.end_date::text AS medication_end_date, medications.notes AS medication_notes, $1::date::text AS scheduled_on, dose.key AS administration_slot, dose.label AS administration_label, dose.time AS administration_time FROM medications INNER JOIN birds ON birds.id = medications.bird_id INNER JOIN workspaces ON workspaces.id = birds.workspace_id CROSS JOIN LATERAL jsonb_to_recordset(medications.dose_schedule) AS dose(key text, label text, time text) 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 medications.reminders_enabled = TRUE AND birds.memorialized_at IS NULL AND medications.start_date <= $1::date AND (medications.end_date IS NULL OR medications.end_date >= $1::date) AND COALESCE(NULLIF(BTRIM(dose.time), ''), '') <> '' AND dose.time <= $2 AND NOT EXISTS ( SELECT 1 FROM medication_reminder_deliveries deliveries WHERE deliveries.medication_id = medications.id AND deliveries.scheduled_on = $1::date AND deliveries.administration_slot = dose.key ) ORDER BY workspaces.name ASC, birds.name ASC, dose.time ASC, medications.name ASC`, [runDate, currentTime], ); return result.rows; }; export const createMedicationReminderDelivery = async ({ medicationId, birdId, workspaceId, scheduledOn, administrationSlot, }: { medicationId: string; birdId: string; workspaceId: number; scheduledOn: string; administrationSlot: string; }) => { const result = await db.query( `INSERT INTO medication_reminder_deliveries (medication_id, bird_id, workspace_id, scheduled_on, administration_slot) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (medication_id, scheduled_on, administration_slot) DO NOTHING RETURNING id, medication_id, bird_id, workspace_id, scheduled_on::text, administration_slot, delivered_at`, [medicationId, birdId, workspaceId, scheduledOn, administrationSlot], ); return result.rows[0] ?? null; }; export const createBird = async ({ birdId, workspaceId, name, tagId, species, motivators, demotivators, favoriteSnack, locationLabel = null, locationDetails = null, vetClinicName = null, vetClinicAddress = null, vetAccountNumber = null, vetDoctorName = null, gender, dateOfBirth, gotchaDay, chartColor, photoDataUrl, photoObjectKey = null, photoContentType = null, photoUpdatedAt = null, notifyOnDob, notifyOnGotchaDay, publicProfileCode = null, publicProfileEnabled = false, }: { birdId?: string; workspaceId: number; name: string; tagId: string | null; species: string; motivators: string | null; demotivators: string | null; favoriteSnack: string | null; locationLabel?: string | null; locationDetails?: Record | null; vetClinicName?: string | null; vetClinicAddress?: string | null; vetAccountNumber?: string | null; vetDoctorName?: string | null; gender: BirdGender; dateOfBirth: string | null; gotchaDay: string | null; chartColor: string; photoDataUrl: string | null; photoObjectKey?: string | null; photoContentType?: string | null; photoUpdatedAt?: string | null; notifyOnDob: boolean; notifyOnGotchaDay: boolean; publicProfileCode?: string | null; publicProfileEnabled?: boolean; }) => { const result = await db.query( `INSERT INTO birds (id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, location_label, location_details, vet_clinic_name, vet_clinic_address, vet_account_number, vet_doctor_name, gender, date_of_birth, gotcha_day, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled) VALUES (COALESCE($1::uuid, gen_random_uuid()), $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26) RETURNING id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, location_label, location_details, vet_clinic_name, vet_clinic_address, vet_account_number, vet_doctor_name, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled, 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`, [ birdId ?? null, workspaceId, name, tagId, species, motivators, demotivators, favoriteSnack, locationLabel, locationDetails, vetClinicName, vetClinicAddress, vetAccountNumber, vetDoctorName, gender, dateOfBirth, gotchaDay, chartColor, photoDataUrl, photoObjectKey, photoContentType, photoUpdatedAt, notifyOnDob, notifyOnGotchaDay, publicProfileCode, publicProfileEnabled, ], ); return result.rows[0] ?? null; }; export const updateBird = async ({ birdId, workspaceId, name, tagId, species, motivators, demotivators, favoriteSnack, locationLabel, locationDetails, vetClinicName, vetClinicAddress, vetAccountNumber, vetDoctorName, gender, dateOfBirth, gotchaDay, chartColor, photoDataUrl, photoObjectKey = null, photoContentType = null, photoUpdatedAt = null, notifyOnDob, notifyOnGotchaDay, publicProfileCode, publicProfileEnabled, }: { birdId: string; workspaceId: number; name: string; tagId: string | null; species: string; motivators: string | null; demotivators: string | null; favoriteSnack: string | null; locationLabel: string | null; locationDetails?: Record | null; vetClinicName: string | null; vetClinicAddress: string | null; vetAccountNumber: string | null; vetDoctorName: string | null; gender: BirdGender; dateOfBirth: string | null; gotchaDay: string | null; chartColor: string; photoDataUrl: string | null; photoObjectKey?: string | null; photoContentType?: string | null; photoUpdatedAt?: string | null; notifyOnDob: boolean; notifyOnGotchaDay: boolean; publicProfileCode: string | null; publicProfileEnabled: boolean; }) => { const result = await db.query( `UPDATE birds SET name = $2, tag_id = $3, species = $4, motivators = $5, demotivators = $6, favorite_snack = $7, location_label = $8, vet_clinic_name = $9, vet_clinic_address = $10, vet_account_number = $11, vet_doctor_name = $12, gender = $13, date_of_birth = $14, gotcha_day = $15, chart_color = $16, photo_data_url = $17, photo_object_key = $18, photo_content_type = $19, photo_updated_at = $20, notify_on_dob = $21, notify_on_gotcha_day = $22, public_profile_code = $23, public_profile_enabled = $24, location_details = $25 WHERE id = $1 AND workspace_id = $26 AND memorialized_at IS NULL RETURNING id, workspace_id, name, tag_id, species, motivators, demotivators, favorite_snack, location_label, location_details, vet_clinic_name, vet_clinic_address, vet_account_number, vet_doctor_name, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled, 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, motivators, demotivators, favoriteSnack, locationLabel, vetClinicName, vetClinicAddress, vetAccountNumber, vetDoctorName, gender, dateOfBirth, gotchaDay, chartColor, photoDataUrl, photoObjectKey, photoContentType, photoUpdatedAt, notifyOnDob, notifyOnGotchaDay, publicProfileCode, publicProfileEnabled, locationDetails ?? null, 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, motivators, demotivators, favorite_snack, location_label, location_details, vet_clinic_name, vet_clinic_address, vet_account_number, vet_doctor_name, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled, 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, motivators, demotivators, favorite_snack, location_label, location_details, vet_clinic_name, vet_clinic_address, vet_account_number, vet_doctor_name, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled, 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, motivators, demotivators, favorite_snack, location_label, location_details, vet_clinic_name, vet_clinic_address, vet_account_number, vet_doctor_name, gender, date_of_birth::text, gotcha_day::text, chart_color, photo_data_url, photo_object_key, photo_content_type, photo_updated_at, notify_on_dob, notify_on_gotcha_day, public_profile_code, public_profile_enabled, 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); try { await createBirdTimelineEvent({ birdId: bird.id, eventType: 'transferred', fromWorkspaceId: transfer.source_workspace_id, toWorkspaceId: targetWorkspaceId, createdByUserId: transfer.requested_by_user_id, }); } catch (timelineError) { console.error('Unable to write bird timeline event', timelineError); } completed += 1; } catch (error) { failed += 1; const message = typeof error === 'object' && error && 'code' in error && error.code === '23505' ? 'That band/tag ID is already in use in FlockPal.' : error instanceof Error ? error.message : 'Unable to complete pending bird transfer.'; await markPendingBirdTransferFailed(transfer.id, message); } } return { completed, failed }; }; export const createBirdTransferCode = async ({ code, birdId, sourceWorkspaceId, requestedByUserId, }: { code: string; birdId: string; sourceWorkspaceId: number; requestedByUserId: string; }) => { await db.query( `UPDATE bird_transfer_codes SET revoked_at = CURRENT_TIMESTAMP WHERE bird_id = $1 AND source_workspace_id = $2 AND completed_at IS NULL AND revoked_at IS NULL`, [birdId, sourceWorkspaceId], ); const result = await db.query( `INSERT INTO bird_transfer_codes (code, bird_id, source_workspace_id, requested_by_user_id) VALUES ($1, $2, $3, $4) RETURNING id, code, bird_id, source_workspace_id, requested_by_user_id, completed_at::text, completed_workspace_id, revoked_at::text, created_at`, [code, birdId, sourceWorkspaceId, requestedByUserId], ); return result.rows[0] ?? null; }; export const getOpenBirdTransferCode = async (code: string) => { const result = await db.query< BirdRow & { transfer_code_id: string; code: string; source_workspace_id: number; requested_by_user_id: string; completed_at: string | null; completed_workspace_id: number | null; revoked_at: string | null; transfer_code_created_at: string; workspace_name: string; } >( `SELECT bird_transfer_codes.id AS transfer_code_id, bird_transfer_codes.code, bird_transfer_codes.source_workspace_id, bird_transfer_codes.requested_by_user_id, bird_transfer_codes.completed_at::text, bird_transfer_codes.completed_workspace_id, bird_transfer_codes.revoked_at::text, bird_transfer_codes.created_at AS transfer_code_created_at, workspaces.name AS workspace_name, ${birdSelectFields} FROM bird_transfer_codes INNER JOIN birds ON birds.id = bird_transfer_codes.bird_id INNER JOIN workspaces ON workspaces.id = bird_transfer_codes.source_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 bird_transfer_codes.code = $1 AND bird_transfer_codes.completed_at IS NULL AND bird_transfer_codes.revoked_at IS NULL AND birds.workspace_id = bird_transfer_codes.source_workspace_id AND birds.memorialized_at IS NULL`, [code], ); return result.rows[0] ?? null; }; export const markBirdTransferCodeCompleted = async (codeId: string, completedWorkspaceId: number) => { await db.query( `UPDATE bird_transfer_codes SET completed_at = CURRENT_TIMESTAMP, completed_workspace_id = $2 WHERE id = $1`, [codeId, completedWorkspaceId], ); }; 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 updateWeightForBird = async ( weightId: string, birdId: string, weightGrams: number, recordedOn: string, notes: string | null, ) => { const result = await db.query( `UPDATE weight_records SET weight_grams = $3, recorded_on = $4, notes = $5 WHERE id = $1 AND bird_id = $2 AND id IN ( SELECT recent.id FROM weight_records recent WHERE recent.bird_id = $2 ORDER BY recent.recorded_on DESC, recent.created_at DESC LIMIT 3 ) RETURNING id, bird_id, weight_grams, recorded_on::text, notes`, [weightId, 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, reminders_enabled 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, remindersEnabled: boolean, ) => { const result = await db.query( `INSERT INTO medications (bird_id, name, dosage, frequency, dose_schedule, route, start_date, end_date, notes, reminders_enabled) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id, bird_id, name, dosage, frequency, dose_schedule, route, start_date::text, end_date::text, notes, reminders_enabled`, [birdId, name, dosage, frequency, JSON.stringify(doseSchedule), route, startDate, endDate, notes, remindersEnabled], ); 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, remindersEnabled: boolean, ) => { 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, reminders_enabled = $11 WHERE id = $1 AND bird_id = $2 RETURNING id, bird_id, name, dosage, frequency, dose_schedule, route, start_date::text, end_date::text, notes, reminders_enabled`, [medicationId, birdId, name, dosage, frequency, JSON.stringify(doseSchedule), route, startDate, endDate, notes, remindersEnabled], ); 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; };