From 905852b8a57cc1d629880047f4d62f2ecbd225f3 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 14 May 2026 03:46:01 +0200 Subject: [PATCH] feat(permissions): carve out dedicated `payments` resource MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Payments (deposit / balance / refund records on an interest) used to share `invoices.record_payment`, which forces a port that doesn't issue invoices at all to still navigate the invoicing permission group to grant its sales reps payment-recording rights. Splitting the resource lets admins gate the two surfaces independently. The new resource has three actions: - view — gates the UI affordance (API reads still go through `interests.view`) - record — POST / PATCH a payment - delete — DELETE a payment record Seed maps updated for all six system roles; existing role rows + per-user permission overrides are backfilled by migration 0064 so upgrades don't silently lose access. Two call sites (POST /interests/ [id]/payments, PATCH /payments/[id]) → payments.record; one (DELETE /payments/[id]) → payments.delete. The PermissionGates on the payments-section UI swap to the new keys. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../api/v1/interests/[id]/payments/route.ts | 2 +- src/app/api/v1/payments/[id]/route.ts | 4 +-- src/components/admin/roles/role-form.tsx | 2 ++ .../admin/users/user-permission-matrix.tsx | 2 ++ src/components/interests/payments-section.tsx | 4 +-- .../migrations/0064_payments_permission.sql | 32 +++++++++++++++++++ src/lib/db/schema/users.ts | 12 +++++++ src/lib/db/seed-permissions.ts | 6 ++++ src/lib/validators/roles.ts | 1 + tests/helpers/factories.ts | 4 +++ 10 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 src/lib/db/migrations/0064_payments_permission.sql diff --git a/src/app/api/v1/interests/[id]/payments/route.ts b/src/app/api/v1/interests/[id]/payments/route.ts index c1a00d09..c7c997c7 100644 --- a/src/app/api/v1/interests/[id]/payments/route.ts +++ b/src/app/api/v1/interests/[id]/payments/route.ts @@ -26,7 +26,7 @@ export const GET = withAuth( ); export const POST = withAuth( - withPermission('invoices', 'record_payment', async (req, ctx, params) => { + withPermission('payments', 'record', async (req, ctx, params) => { try { // Body's interestId must match the URL param — defense-in-depth against // a client that sends one ID in the URL but another in the body. diff --git a/src/app/api/v1/payments/[id]/route.ts b/src/app/api/v1/payments/[id]/route.ts index 1da09aa8..a82eede7 100644 --- a/src/app/api/v1/payments/[id]/route.ts +++ b/src/app/api/v1/payments/[id]/route.ts @@ -7,7 +7,7 @@ import { updatePaymentSchema } from '@/lib/validators/payments'; import { deletePayment, updatePayment } from '@/lib/services/payments.service'; export const PATCH = withAuth( - withPermission('invoices', 'record_payment', async (req, ctx, params) => { + withPermission('payments', 'record', async (req, ctx, params) => { try { const body = await parseBody(req, updatePaymentSchema); const payment = await updatePayment(params.id!, ctx.portId, body, { @@ -24,7 +24,7 @@ export const PATCH = withAuth( ); export const DELETE = withAuth( - withPermission('invoices', 'record_payment', async (_req, ctx, params) => { + withPermission('payments', 'delete', async (_req, ctx, params) => { try { await deletePayment(params.id!, ctx.portId, { userId: ctx.userId, diff --git a/src/components/admin/roles/role-form.tsx b/src/components/admin/roles/role-form.tsx index b6560511..7cf0f254 100644 --- a/src/components/admin/roles/role-form.tsx +++ b/src/components/admin/roles/role-form.tsx @@ -59,6 +59,7 @@ const DEFAULT_PERMISSIONS: Record> = { record_payment: false, export: false, }, + payments: { view: false, record: false, delete: false }, files: { view: false, upload: false, edit: false, delete: false, manage_folders: false }, email: { view: false, send: false, configure_account: false }, reminders: { @@ -105,6 +106,7 @@ const GROUP_LABELS: Record = { documents: 'Documents', expenses: 'Expenses', invoices: 'Invoices', + payments: 'Payments', files: 'Files', email: 'Email', reminders: 'Reminders', diff --git a/src/components/admin/users/user-permission-matrix.tsx b/src/components/admin/users/user-permission-matrix.tsx index 70bfbcb0..3e481bcc 100644 --- a/src/components/admin/users/user-permission-matrix.tsx +++ b/src/components/admin/users/user-permission-matrix.tsx @@ -36,6 +36,7 @@ const GROUP_LABELS: Record = { documents: 'Documents', expenses: 'Expenses', invoices: 'Invoices', + payments: 'Payments', files: 'Files', email: 'Email', reminders: 'Reminders', @@ -78,6 +79,7 @@ const PERMISSION_LEAVES: Record = { ], expenses: ['view', 'create', 'edit', 'delete', 'export', 'scan_receipt'], invoices: ['view', 'create', 'edit', 'delete', 'send', 'record_payment', 'export'], + payments: ['view', 'record', 'delete'], files: ['view', 'upload', 'edit', 'delete', 'manage_folders'], email: ['view', 'send', 'configure_account'], reminders: ['view_own', 'view_all', 'create', 'edit_own', 'edit_all', 'assign_others'], diff --git a/src/components/interests/payments-section.tsx b/src/components/interests/payments-section.tsx index 8fbbf9bb..2bd12504 100644 --- a/src/components/interests/payments-section.tsx +++ b/src/components/interests/payments-section.tsx @@ -128,7 +128,7 @@ export function PaymentsSection({ that.

- +