From fa614b8c375e0cbd53bfc175e4ac3a0d885416ad Mon Sep 17 00:00:00 2001 From: NeilClack Date: Fri, 11 Sep 2026 12:43:41 -0500 Subject: [PATCH] Fix staff(+) unable to edit or delete addresses --- .../admin/profiles/+page.server.ts | 40 ++++- .../admin/profiles/EditProfileModal.svelte | 60 +++++++- .../admin/profiles/page.server.spec.ts | 138 ++++++++++++++++++ 3 files changed, 234 insertions(+), 4 deletions(-) diff --git a/src/routes/(protected)/admin/profiles/+page.server.ts b/src/routes/(protected)/admin/profiles/+page.server.ts index 2e9ea6f..80fca67 100644 --- a/src/routes/(protected)/admin/profiles/+page.server.ts +++ b/src/routes/(protected)/admin/profiles/+page.server.ts @@ -22,6 +22,7 @@ const ProfileSchema = z.object({ }); const AddressSchema = z.object({ + id: z.string().optional(), profileId: z.string().min(1), street: z.string().min(1, 'Street is required'), city: z.string().min(1, 'City is required'), @@ -147,8 +148,14 @@ export const actions = { }; } - const { profileId, isDefaultMailing, isDefaultBilling, isDefaultShipping, ...addressData } = - form.data; + const { + id, + profileId, + isDefaultMailing, + isDefaultBilling, + isDefaultShipping, + ...addressData + } = form.data; if (!profileId) { return { success: false, action: 'upsertAddress', addressError: 'Missing profile ID' }; @@ -181,7 +188,36 @@ export const actions = { isDefaultShipping }; + if (id) { + const existing = await prisma.address.findFirst({ where: { id, profileId } }); + if (!existing) { + return { success: false, action: 'upsertAddress', addressError: 'Address not found' }; + } + const updated = await prisma.address.update({ where: { id }, data: payload }); + return { success: true, action: 'upsertAddress', address: updated }; + } + const created = await prisma.address.create({ data: payload }); return { success: true, action: 'upsertAddress', address: created }; + }, + deleteAddress: async ({ request, locals }) => { + if (!isStaff(locals)) { + return { success: false, action: 'deleteAddress', addressError: 'Not authorized' }; + } + + const formData = await request.formData(); + const id = formData.get('id')?.toString(); + if (!id) { + return { success: false, action: 'deleteAddress', addressError: 'Missing address id' }; + } + + try { + await prisma.address.delete({ where: { id } }); + } catch (err) { + console.error('Address delete error:', err); + return { success: false, action: 'deleteAddress', addressError: 'Failed to delete address' }; + } + + return { success: true, action: 'deleteAddress', addressId: id }; } }; diff --git a/src/routes/(protected)/admin/profiles/EditProfileModal.svelte b/src/routes/(protected)/admin/profiles/EditProfileModal.svelte index a256de1..44d766c 100644 --- a/src/routes/(protected)/admin/profiles/EditProfileModal.svelte +++ b/src/routes/(protected)/admin/profiles/EditProfileModal.svelte @@ -11,6 +11,7 @@ fieldErrors?: Record; addressError?: string; address?: Record; + addressId?: string; } | null; let { @@ -32,6 +33,7 @@ let addressError = $state(''); let successMessage = $state(''); let addressFormOpen = $state(false); + let editingAddressId = $state(null); let addresses = $state[]>([]); let addrStreet = $state(''); @@ -49,6 +51,7 @@ let lastForm = $state.raw(untrack(() => form ?? null)); function resetAddressFields() { + editingAddressId = null; addrStreet = ''; addrCity = ''; addrState = ''; @@ -89,7 +92,11 @@ switch (form.action) { case 'upsertAddress': if (form.success && form.address) { - addresses = [...addresses, form.address]; + const savedId = String(form.address.id ?? ''); + const exists = addresses.some((a) => String(a.id ?? '') === savedId); + addresses = exists + ? addresses.map((a) => (String(a.id ?? '') === savedId ? form.address! : a)) + : [...addresses, form.address]; addressFormOpen = false; addressError = ''; successMessage = 'Address saved'; @@ -99,6 +106,16 @@ addressError = form.addressError ?? ''; } break; + case 'deleteAddress': + if (form.success) { + addresses = addresses.filter((a) => String(a.id ?? '') !== String(form.addressId ?? '')); + addressError = ''; + successMessage = 'Address deleted'; + } else { + successMessage = ''; + addressError = form.addressError ?? ''; + } + break; case 'upsert': default: if (form.success) { @@ -119,6 +136,21 @@ addressError = ''; if (addressFormOpen) resetAddressFields(); } + + function openEditAddress(addr: Record) { + resetAddressFields(); + editingAddressId = String(addr.id ?? ''); + addrStreet = String(addr.street ?? ''); + addrCity = String(addr.city ?? ''); + addrState = String(addr.state ?? ''); + addrPostal = String(addr.postalCode ?? ''); + addrCountry = String(addr.country ?? ''); + addrMailing = Boolean(addr.isDefaultMailing); + addrBilling = Boolean(addr.isDefaultBilling); + addrShipping = Boolean(addr.isDefaultShipping); + addressError = ''; + addressFormOpen = true; + } @@ -172,6 +204,9 @@ {#if addressFormOpen}
+ {#if editingAddressId} + + {/if} {#if addressError}
{addressError} @@ -262,7 +297,7 @@ type="submit" class="rounded-md bg-slate-800 px-4 py-2 text-sm font-medium text-white transition hover:bg-slate-700" > - Save Address + {editingAddressId ? 'Update Address' : 'Save Address'}
@@ -283,6 +318,7 @@ M B S + Actions @@ -296,6 +332,26 @@ {addr.isDefaultMailing ? '✓' : ''} {addr.isDefaultBilling ? '✓' : ''} {addr.isDefaultShipping ? '✓' : ''} + +
+ +
+ + +
+
+ {/each} diff --git a/src/routes/(protected)/admin/profiles/page.server.spec.ts b/src/routes/(protected)/admin/profiles/page.server.spec.ts index 6eb57ec..19f8215 100644 --- a/src/routes/(protected)/admin/profiles/page.server.spec.ts +++ b/src/routes/(protected)/admin/profiles/page.server.spec.ts @@ -14,6 +14,12 @@ vi.mock('$lib/db', () => ({ findUnique: vi.fn(), findFirst: vi.fn(), update: vi.fn() + }, + address: { + findFirst: vi.fn(), + create: vi.fn(), + update: vi.fn(), + delete: vi.fn() } } })); @@ -29,6 +35,10 @@ const mockProfileDelete = prisma.profile.delete as ReturnType; const mockUserFindUnique = prisma.user.findUnique as ReturnType; const mockUserFindFirst = prisma.user.findFirst as ReturnType; const mockUserUpdate = prisma.user.update as ReturnType; +const mockAddressFindFirst = prisma.address.findFirst as ReturnType; +const mockAddressCreate = prisma.address.create as ReturnType; +const mockAddressUpdate = prisma.address.update as ReturnType; +const mockAddressDelete = prisma.address.delete as ReturnType; const validFields = { email: 'jane@example.com', @@ -242,4 +252,132 @@ describe('profiles admin page server', () => { expect(result).toEqual({ success: false, error: 'Missing id' }); }); }); + + describe('upsertAddress', () => { + const addressFields = { + profileId: 'p1', + street: '123 Main St', + city: 'Dallas', + state: 'TX', + postalCode: '75001', + country: 'US' + }; + + it('rejects non-staff users', async () => { + const result = await actions.upsertAddress({ + request: createFormRequest(addressFields), + locals: createLocals(mockUser({ roleId: 4 }) as any) + } as any); + + expect(result).toEqual({ + success: false, + action: 'upsertAddress', + addressError: 'Not authorized' + }); + }); + + it('creates a new address when no id is supplied', async () => { + const created = { id: 'a1', ...addressFields }; + mockAddressCreate.mockResolvedValue(created); + + const result = await actions.upsertAddress({ + request: createFormRequest(addressFields), + locals: staff() + } as any); + + expect(result).toEqual({ success: true, action: 'upsertAddress', address: created }); + expect(mockAddressCreate).toHaveBeenCalledWith({ + data: { + profileId: 'p1', + street: '123 Main St', + city: 'Dallas', + state: 'TX', + postalCode: '75001', + country: 'US', + type: 'Mailing', + isDefaultMailing: false, + isDefaultBilling: false, + isDefaultShipping: false + } + }); + }); + + it('updates an existing address when an id is supplied', async () => { + mockAddressFindFirst.mockResolvedValue({ id: 'a1' }); + const updated = { id: 'a1', ...addressFields, street: '456 Oak Ave' }; + mockAddressUpdate.mockResolvedValue(updated); + + const result = await actions.upsertAddress({ + request: createFormRequest({ ...addressFields, id: 'a1', street: '456 Oak Ave' }), + locals: staff() + } as any); + + expect(result).toEqual({ success: true, action: 'upsertAddress', address: updated }); + expect(mockAddressFindFirst).toHaveBeenCalledWith({ + where: { id: 'a1', profileId: 'p1' } + }); + expect(mockAddressUpdate).toHaveBeenCalledWith({ + where: { id: 'a1' }, + data: expect.objectContaining({ street: '456 Oak Ave', profileId: 'p1' }) + }); + expect(mockAddressCreate).not.toHaveBeenCalled(); + }); + + it('errors when updating an address that does not belong to the profile', async () => { + mockAddressFindFirst.mockResolvedValue(null); + + const result = await actions.upsertAddress({ + request: createFormRequest({ ...addressFields, id: 'a1' }), + locals: staff() + } as any); + + expect(result).toEqual({ + success: false, + action: 'upsertAddress', + addressError: 'Address not found' + }); + expect(mockAddressUpdate).not.toHaveBeenCalled(); + }); + }); + + describe('deleteAddress', () => { + it('rejects non-staff users', async () => { + const result = await actions.deleteAddress({ + request: createFormRequest({ id: 'a1' }), + locals: createLocals(mockUser({ roleId: 4 }) as any) + } as any); + + expect(result).toEqual({ + success: false, + action: 'deleteAddress', + addressError: 'Not authorized' + }); + expect(mockAddressDelete).not.toHaveBeenCalled(); + }); + + it('deletes the address by id for staff', async () => { + mockAddressDelete.mockResolvedValue({}); + + const result = await actions.deleteAddress({ + request: createFormRequest({ id: 'a1' }), + locals: staff() + } as any); + + expect(result).toEqual({ success: true, action: 'deleteAddress', addressId: 'a1' }); + expect(mockAddressDelete).toHaveBeenCalledWith({ where: { id: 'a1' } }); + }); + + it('returns an error when id is missing', async () => { + const result = await actions.deleteAddress({ + request: createFormRequest({}), + locals: staff() + } as any); + + expect(result).toEqual({ + success: false, + action: 'deleteAddress', + addressError: 'Missing address id' + }); + }); + }); });