fix: hide API-archived customers and suppliers from lists and pickers (#1927)

* fix: hide API-archived customers and suppliers from lists and pickers

The v1 API soft-archives customers and suppliers (archived_at, plus
is_active=false on suppliers) and its own list routes hide those rows
behind ?include_archived=true. No other surface filtered archived_at, so
an archived counterparty stayed a normal row in the dashboard rosters,
the internal /api/customers and /api/suppliers list routes, the MCP list
tools and every customer/supplier picker.

Apply the same canonical `archived_at IS NULL` filter on every non-v1
list and picker path:

- /api/customers GET, /api/suppliers GET (feeds the customers page and
  the supplier-invoice form)
- suppliers dashboard page (reads suppliers via browser Supabase)
- InvoiceEditor and NewRecurringScheduleDialog customer pickers; an
  invoice or schedule being edited keeps its current customer visible
  (archiving does not refuse on drafts, so a draft can point at one)
- deadlines page and CalendarWorkspace customer pickers
- InvoicePreviewCard sample customer
- gnubok_list_customers and gnubok_list_suppliers: hidden by default,
  optional include_archived boolean mirroring the v1 flag; rows now
  carry archived_at so an agent can tell them apart when opted in

Detail routes and by-id lookups are untouched: an archived row still
opens. The delete-vs-archive semantics are unchanged.

The tools/list payload guard moves 60.7K to 60.8K: main had ~6 tokens
of headroom, so even the bare boolean contract crossed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(schema): raise the unresolvable-expression ceiling by 2 for the archived-customer picker filters

The two .or('archived_at.is.null,id.eq.<uuid>') filters keep an edited
draft's archived customer selectable. The uuid is a runtime value, so the
scanner cannot resolve the expression; both columns exist and the filter is
covered by the archived-counterparty tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-08-26 13:35:27 +02:00
committed by GitHub
co-authored by Claude Fable 5 Jakob Wennberg
parent 1185ab4294
commit f338850bd0
14 changed files with 273 additions and 27 deletions
@@ -39,6 +39,7 @@ export default function CalendarWorkspace({ userId }: WorkspaceComponentProps) {
const { data: customersData, error: customersError } = await supabase
.from('customers')
.select('id, name')
.is('archived_at', null)
.order('name', { ascending: true })
if (customersError) throw customersError
+10 -5
View File
@@ -1042,11 +1042,16 @@ export default function InvoiceEditor(props: InvoiceEditorProps = { mode: 'creat
async function fetchCustomers() {
if (!company?.id) return
const { data, error } = await supabase
.from('customers')
.select('*')
.eq('company_id', company.id)
.order('name', { ascending: true })
// Archived customers (v1 API soft-delete) are not offered in the picker.
// An existing draft or copied invoice may still point at one (archiving
// only refuses when open invoices exist, drafts do not count), so that
// single row is kept in the list or the select would render blank.
const keepCustomerId = initial?.customer_id ?? copyInitial?.customer_id ?? null
const base = supabase.from('customers').select('*').eq('company_id', company.id)
const query = keepCustomerId
? base.or(`archived_at.is.null,id.eq.${keepCustomerId}`)
: base.is('archived_at', null)
const { data, error } = await query.order('name', { ascending: true })
if (error) {
toast({
@@ -178,13 +178,14 @@ function NewRecurringScheduleForm({
useEffect(() => {
if (!company) return
supabase
.from('customers')
.select('*')
.eq('company_id', company.id)
.order('name')
.then(({ data }) => setCustomers(data ?? []))
}, [company])
// Archived customers (v1 API soft-delete) are not offered in the picker,
// but a schedule being edited keeps its current customer visible.
const base = supabase.from('customers').select('*').eq('company_id', company.id)
const query = schedule?.customer_id
? base.or(`archived_at.is.null,id.eq.${schedule.customer_id}`)
: base.is('archived_at', null)
query.order('name').then(({ data }) => setCustomers(data ?? []))
}, [company, schedule?.customer_id])
async function onSubmit(data: FormData) {
setIsSubmitting(true)
@@ -56,6 +56,7 @@ export function InvoicePreviewCard({ settings }: InvoicePreviewCardProps) {
.from('customers')
.select('id')
.eq('company_id', companyId)
.is('archived_at', null)
.limit(1)
.maybeSingle()