feat(mcp): customer_number on create_customer + Beta tags on webshop surfaces (#1677)
* feat(mcp): accept customer_number on gnubok_create_customer Parity with gnubok_update_customer: a customer number no longer needs a create-then-update two-step with two approvals. The staged params carry the trimmed number, commitCreateCustomer inserts it, and the payload-size ceiling is bumped 59.7K to 59.75K with a documented entry (the property has no description; name + maxLength are the whole contract). Requested by a user on Discord 2026-08-16. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(ui): mark webshop integrations and orders tab as Beta WooCommerce and Shopify rows on the import page get a quiet Beta chip next to the title, and the webshop /orders sidebar item sets the existing betaBadge flag. Chip recipe matches the nav beta badge so Beta reads identically everywhere. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(mcp): enforce customer_number invariants and show it on the approval card Consolidated resolution pass for PR #1677: - skeptic (correctness): maxLength 32 was advertisement-only on the create path; now enforced with a runtime guard in gnubok_create_customer execute (clean errors for non-string and >32) and a 400 guard in commitCreateCustomer, matching the web/v1 routes and commitUpdateCustomer. - skeptic (correctness): CustomerPreview never rendered the staged customer_number, leaving the approver blind to the new field; added a conditional Kundnr row. - CodeRabbit: reset the event bus in create-customer.test.ts beforeEach. - Tests cover both new guards at the tool and executor layers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
387e1fb7f1
commit
cfdddb2d7e
@@ -204,6 +204,79 @@ describe('commitPendingOperation: unlock_period', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ─── create_customer ────────────────────────────────────────────────
|
||||
|
||||
describe('commitPendingOperation: create_customer', () => {
|
||||
it('inserts the staged customer_number', async () => {
|
||||
const { supabase, enqueue, findCall } = createQueuedMockSupabase()
|
||||
enqueue({ data: { id: 'op-1' }, error: null }) // CAS claim
|
||||
enqueue({
|
||||
data: makeCustomer({ id: 'cust-1', customer_number: 'K-1001' }),
|
||||
error: null,
|
||||
}) // customers insert
|
||||
enqueue({ data: null, error: null }) // dispatcher's pending_operations update
|
||||
|
||||
const op = makePendingOp({
|
||||
operation_type: 'create_customer',
|
||||
params: {
|
||||
name: 'Kund AB',
|
||||
customer_type: 'swedish_business',
|
||||
customer_number: 'K-1001',
|
||||
email: 'faktura@example.test',
|
||||
},
|
||||
})
|
||||
|
||||
const result = await commitPendingOperation(supabase as never, 'user-1', 'company-1', op)
|
||||
|
||||
expect(result.status).toBe('committed')
|
||||
expect(findCall('customers', 'insert')?.[0]).toMatchObject({
|
||||
company_id: 'company-1',
|
||||
name: 'Kund AB',
|
||||
customer_number: 'K-1001',
|
||||
email: 'faktura@example.test',
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects a staged customer_number longer than 32 characters without inserting', async () => {
|
||||
const { supabase, enqueue, findCall } = createQueuedMockSupabase()
|
||||
enqueue({ data: { id: 'op-1' }, error: null }) // CAS claim
|
||||
enqueue({ data: null, error: null }) // dispatcher's reject update
|
||||
|
||||
const op = makePendingOp({
|
||||
operation_type: 'create_customer',
|
||||
params: {
|
||||
name: 'Kund AB',
|
||||
customer_type: 'swedish_business',
|
||||
customer_number: 'X'.repeat(33),
|
||||
},
|
||||
})
|
||||
|
||||
const result = await commitPendingOperation(supabase as never, 'user-1', 'company-1', op)
|
||||
|
||||
expect(result.status).toBe('failed')
|
||||
expect(result.http_status).toBe(400)
|
||||
expect(result.error).toMatch(/32/)
|
||||
expect(findCall('customers', 'insert')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('inserts customer_number as null when not staged', async () => {
|
||||
const { supabase, enqueue, findCall } = createQueuedMockSupabase()
|
||||
enqueue({ data: { id: 'op-1' }, error: null }) // CAS claim
|
||||
enqueue({ data: makeCustomer({ id: 'cust-1' }), error: null }) // customers insert
|
||||
enqueue({ data: null, error: null }) // dispatcher's pending_operations update
|
||||
|
||||
const op = makePendingOp({
|
||||
operation_type: 'create_customer',
|
||||
params: { name: 'Kund AB', customer_type: 'swedish_business' },
|
||||
})
|
||||
|
||||
const result = await commitPendingOperation(supabase as never, 'user-1', 'company-1', op)
|
||||
|
||||
expect(result.status).toBe('committed')
|
||||
expect(findCall('customers', 'insert')?.[0]).toMatchObject({ customer_number: null })
|
||||
})
|
||||
})
|
||||
|
||||
describe('commitPendingOperation: credit-note issuance guard', () => {
|
||||
it('rejects mark_invoice_sent before the ordinary invoice executor can book it', async () => {
|
||||
const { supabase, enqueue } = createQueuedMockSupabase()
|
||||
|
||||
@@ -347,6 +347,13 @@ async function commitCreateCustomer(
|
||||
companyId: string,
|
||||
params: Record<string, unknown>
|
||||
): Promise<ExecutorResult> {
|
||||
// Same 32-char invariant the web/v1 create routes and commitUpdateCustomer
|
||||
// enforce; MCP hosts don't reliably enforce inputSchema maxLength.
|
||||
const customerNumber = params.customer_number
|
||||
if (customerNumber != null && (typeof customerNumber !== 'string' || customerNumber.length > 32)) {
|
||||
return { error: 'customer_number must be a string of at most 32 characters', status: 400 }
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('customers')
|
||||
.insert({
|
||||
@@ -354,6 +361,7 @@ async function commitCreateCustomer(
|
||||
company_id: companyId,
|
||||
name: params.name as string,
|
||||
customer_type: params.customer_type as string,
|
||||
customer_number: customerNumber || null,
|
||||
email: (params.email as string) || null,
|
||||
org_number: (params.org_number as string) || null,
|
||||
vat_number: (params.vat_number as string) || null,
|
||||
|
||||
Reference in New Issue
Block a user