Files
Bookra/apps/backend/internal/db/locations.go
T
Tomas DvorakandDevin f2f7439050 refactor(db): replace sqlc with handwritten pgx repos
- Finish Paddle-to-Stripe billing migration; drop paddle/stripe libs
- Renumber migrations; add 00012 memberships+manage_token,
  00013 location_type_capacity
- Add tests: admin, auth, bookings customer service
- Remove graphify-out artifacts and stale env files

Authored By: TDvorak <[email protected]>

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-17 20:21:34 +02:00

176 lines
6.2 KiB
Go

package db
import (
"context"
"time"
)
const locationColumns = "id, tenant_id, name, type, capacity, timezone, created_at"
func (r *PGRepository) ListLocationsByTenant(ctx context.Context, tenantID string) ([]LocationRecord, error) {
rows, err := r.pool.Query(ctx, `
SELECT `+locationColumns+`
FROM locations
WHERE tenant_id = $1
ORDER BY name
`, tenantID)
if err != nil {
return nil, err
}
defer rows.Close()
var records []LocationRecord
for rows.Next() {
var rec LocationRecord
if err := rows.Scan(&rec.ID, &rec.TenantID, &rec.Name, &rec.Type, &rec.Capacity, &rec.Timezone, &rec.CreatedAt); err != nil {
return nil, err
}
records = append(records, rec)
}
return records, rows.Err()
}
func (r *PGRepository) GetLocationByID(ctx context.Context, locationID string) (LocationRecord, error) {
var rec LocationRecord
err := r.pool.QueryRow(ctx, `
SELECT `+locationColumns+`
FROM locations
WHERE id = $1
`, locationID).Scan(&rec.ID, &rec.TenantID, &rec.Name, &rec.Type, &rec.Capacity, &rec.Timezone, &rec.CreatedAt)
return rec, err
}
func (r *PGRepository) CreateLocation(ctx context.Context, params CreateLocationParams) (LocationRecord, error) {
var rec LocationRecord
err := r.pool.QueryRow(ctx, `
INSERT INTO locations (tenant_id, name, type, capacity, timezone)
VALUES ($1, $2, $3, $4, $5)
RETURNING `+locationColumns+`
`, params.TenantID, params.Name, params.Type, params.Capacity, params.Timezone).Scan(&rec.ID, &rec.TenantID, &rec.Name, &rec.Type, &rec.Capacity, &rec.Timezone, &rec.CreatedAt)
return rec, err
}
func (r *PGRepository) UpdateLocation(ctx context.Context, locationID string, params UpdateLocationParams) (LocationRecord, error) {
var rec LocationRecord
err := r.pool.QueryRow(ctx, `
UPDATE locations
SET name = COALESCE($2, name),
type = COALESCE($3, type),
capacity = COALESCE($4, capacity),
timezone = COALESCE($5, timezone),
updated_at = now()
WHERE id = $1
RETURNING `+locationColumns+`
`, locationID, params.Name, params.Type, params.Capacity, params.Timezone).Scan(&rec.ID, &rec.TenantID, &rec.Name, &rec.Type, &rec.Capacity, &rec.Timezone, &rec.CreatedAt)
return rec, err
}
func (r *PGRepository) DeleteLocation(ctx context.Context, locationID string) error {
_, err := r.pool.Exec(ctx, `DELETE FROM locations WHERE id = $1`, locationID)
return err
}
func (r *PGRepository) ListBlockedDaysByTenant(ctx context.Context, tenantID string, from time.Time, to time.Time) ([]BlockedDayRecord, error) {
rows, err := r.pool.Query(ctx, `
SELECT id, tenant_id, staff_id, starts_at, ends_at, kind, reason, created_at
FROM availability_exceptions
WHERE tenant_id = $1 AND starts_at <= $3 AND ends_at >= $2
ORDER BY starts_at
`, tenantID, from, to)
if err != nil {
return nil, err
}
defer rows.Close()
var records []BlockedDayRecord
for rows.Next() {
var rec BlockedDayRecord
if err := rows.Scan(&rec.ID, &rec.TenantID, &rec.StaffID, &rec.StartsAt, &rec.EndsAt, &rec.Kind, &rec.Reason, &rec.CreatedAt); err != nil {
return nil, err
}
records = append(records, rec)
}
return records, rows.Err()
}
func (r *PGRepository) CreateBlockedDay(ctx context.Context, params CreateBlockedDayParams) (BlockedDayRecord, error) {
var rec BlockedDayRecord
err := r.pool.QueryRow(ctx, `
INSERT INTO availability_exceptions (tenant_id, staff_id, starts_at, ends_at, kind, reason)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id, tenant_id, staff_id, starts_at, ends_at, kind, reason, created_at
`, params.TenantID, params.StaffID, params.StartsAt, params.EndsAt, params.Kind, params.Reason).Scan(&rec.ID, &rec.TenantID, &rec.StaffID, &rec.StartsAt, &rec.EndsAt, &rec.Kind, &rec.Reason, &rec.CreatedAt)
return rec, err
}
func (r *PGRepository) UpdateBlockedDay(ctx context.Context, blockedDayID string, params UpdateBlockedDayParams) (BlockedDayRecord, error) {
var rec BlockedDayRecord
err := r.pool.QueryRow(ctx, `
UPDATE availability_exceptions
SET starts_at = COALESCE($2, starts_at),
ends_at = COALESCE($3, ends_at),
kind = COALESCE($4, kind),
reason = COALESCE($5, reason)
WHERE id = $1
RETURNING id, tenant_id, staff_id, starts_at, ends_at, kind, reason, created_at
`, blockedDayID, params.StartsAt, params.EndsAt, params.Kind, params.Reason).Scan(&rec.ID, &rec.TenantID, &rec.StaffID, &rec.StartsAt, &rec.EndsAt, &rec.Kind, &rec.Reason, &rec.CreatedAt)
return rec, err
}
func (r *PGRepository) DeleteBlockedDay(ctx context.Context, blockedDayID string) error {
_, err := r.pool.Exec(ctx, `DELETE FROM availability_exceptions WHERE id = $1`, blockedDayID)
return err
}
func (r *PGRepository) ListWorkingHoursByTenant(ctx context.Context, tenantID string) ([]WorkingHoursRecord, error) {
rows, err := r.pool.Query(ctx, `
SELECT tenant_id, staff_id, day_of_week, starts_local::text, ends_local::text
FROM availability_rules
WHERE tenant_id = $1 AND staff_id IS NULL
ORDER BY day_of_week
`, tenantID)
if err != nil {
return nil, err
}
defer rows.Close()
var records []WorkingHoursRecord
for rows.Next() {
var rec WorkingHoursRecord
if err := rows.Scan(&rec.TenantID, &rec.StaffID, &rec.DayOfWeek, &rec.StartsLocal, &rec.EndsLocal); err != nil {
return nil, err
}
records = append(records, rec)
}
return records, rows.Err()
}
func (r *PGRepository) UpdateWorkingHours(ctx context.Context, tenantID string, dayOfWeek int, params UpdateWorkingHoursParams) error {
if params.IsOpen != nil && !*params.IsOpen {
_, err := r.pool.Exec(ctx, `
DELETE FROM availability_rules
WHERE tenant_id = $1 AND day_of_week = $2 AND staff_id IS NULL
`, tenantID, dayOfWeek)
return err
}
if params.StartsLocal == nil || params.EndsLocal == nil {
return nil
}
// availability_rules has no unique constraint on (tenant, day): replace the
// tenant-level row so repeated updates stay idempotent.
if _, err := r.pool.Exec(ctx, `
DELETE FROM availability_rules
WHERE tenant_id = $1 AND day_of_week = $2 AND staff_id IS NULL
`, tenantID, dayOfWeek); err != nil {
return err
}
_, err := r.pool.Exec(ctx, `
INSERT INTO availability_rules (id, tenant_id, staff_id, day_of_week, starts_local, ends_local)
VALUES (gen_random_uuid(), $1, NULL, $2, $3::time, $4::time)
`, tenantID, dayOfWeek, *params.StartsLocal, *params.EndsLocal)
return err
}