@open-mercato/webhooks 0.6.5-develop.5382.1.f542de69af → 0.6.6-develop.5412.1.e2a52b14f0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -137,9 +137,9 @@ test.describe("TC-LOCK-OSS-043: webhooks + inbox settings + data-sync schedule o
137
137
  }
138
138
  });
139
139
  test("WHK-01 stale webhook DELETE in the list surfaces the conflict bar", async ({ page }) => {
140
+ test.setTimeout(6e4);
140
141
  const token = await getAuthToken(page.request, "admin");
141
142
  const stamp = Date.now();
142
- const webhookName = `QA Lock 043 ${stamp}`;
143
143
  let webhookId = null;
144
144
  try {
145
145
  const webhook = await createWebhook(page.request, token, stamp);
@@ -150,7 +150,7 @@ test.describe("TC-LOCK-OSS-043: webhooks + inbox settings + data-sync schedule o
150
150
  (response) => response.url().includes("/api/webhooks") && response.request().method() === "GET" && response.ok(),
151
151
  { timeout: 2e4 }
152
152
  ).catch(() => void 0);
153
- const row = page.getByRole("row", { name: new RegExp(`${webhookName}\\b`) }).first();
153
+ const row = page.getByRole("row", { name: new RegExp(`QA Lock 043 (?:bumped )?${stamp}\\b`) }).first();
154
154
  await expect(row, "created webhook should appear in the list").toBeVisible({ timeout: 2e4 });
155
155
  const bump = await apiRequest(page.request, "PUT", `${WEBHOOKS_API}/${webhookId}`, {
156
156
  token,
@@ -158,11 +158,15 @@ test.describe("TC-LOCK-OSS-043: webhooks + inbox settings + data-sync schedule o
158
158
  });
159
159
  expect(bump.status(), "out-of-band webhook PUT should succeed").toBeLessThan(300);
160
160
  const kebab = row.getByRole("button", { name: /open actions/i });
161
- await expect(kebab, "row should expose an Open actions trigger").toBeVisible();
162
- await kebab.click();
163
161
  const deleteItem = page.getByRole("menuitem", { name: /^delete$/i });
164
- await expect(deleteItem, "delete menu item should be visible").toBeVisible({ timeout: 1e4 });
165
- await deleteItem.click();
162
+ await expect(async () => {
163
+ if (!await deleteItem.isVisible().catch(() => false)) {
164
+ await kebab.click({ timeout: 2e3 }).catch(() => {
165
+ });
166
+ await expect(deleteItem).toBeVisible({ timeout: 1500 });
167
+ }
168
+ await deleteItem.click({ timeout: 2e3 });
169
+ }).toPass({ timeout: 3e4 });
166
170
  const dialog = page.getByRole("alertdialog");
167
171
  await expect(dialog, "confirm dialog should open").toBeVisible({ timeout: 1e4 });
168
172
  await dialog.getByRole("button", { name: /^(confirm|delete)$/i }).click();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/webhooks/__integration__/TC-LOCK-OSS-043.spec.ts"],
4
- "sourcesContent": ["import { test, expect, type APIRequestContext } from '@playwright/test'\nimport { getAuthToken, apiRequest } from '@open-mercato/core/modules/core/__integration__/helpers/api'\nimport { login } from '@open-mercato/core/modules/core/__integration__/helpers/auth'\nimport {\n putWithLock,\n expectConflictBody,\n expectConflictBanner,\n resolveApiUrl,\n} from '@open-mercato/core/modules/core/__integration__/helpers/optimisticLockUi'\nimport { OPTIMISTIC_LOCK_HEADER_NAME } from '@open-mercato/shared/lib/crud/optimistic-lock-headers'\n\n/**\n * TC-LOCK-OSS-043 \u2014 webhooks + inbox settings + data-sync schedule\n * optimistic-lock conflict contract (WHK-01, INB-01, SYNC-01).\n *\n * All three surfaces guard writes with `enforceCommandOptimisticLock` inside\n * pure command/API routes \u2014 none expose a dedicated `data-crud-field-id`\n * CrudForm edit page for the locked record, so the conflict contract is proven\n * at the API level: capture the record's `updated_at`, advance it out-of-band\n * via a header-less write (the strictly-additive path always succeeds and bumps\n * `updated_at`), then replay the now-stale write with the original\n * expected-version header \u2192 409 `optimistic_lock_conflict`.\n *\n * - WHK-01: `PUT /api/webhooks/<id>` and `DELETE /api/webhooks/<id>`\n * (`packages/webhooks/src/modules/webhooks/api/webhooks/[id]/route.ts`,\n * `resourceKind: 'webhooks.endpoint'`).\n * - INB-01: `PATCH /api/inbox_ops/settings`\n * (`packages/core/src/modules/inbox_ops/api/settings/route.ts`,\n * `resourceKind: 'inbox_ops.settings'`). The settings row is a per-tenant\n * singleton, so the test bumps + restores `workingLanguage` instead of\n * creating/deleting a fixture.\n * - SYNC-01: `POST /api/data_sync/schedules`\n * (`packages/core/src/modules/data_sync/api/schedules/route.ts` \u2192\n * `SyncScheduleService.saveSchedule`, `resourceKind: 'data_sync.schedule'`).\n * The collection POST reads the expected-version header; the service enforces\n * the lock when a row with the same (integrationId, entityType, direction)\n * key already exists.\n */\n\nconst WEBHOOKS_API = '/api/webhooks'\nconst INBOX_SETTINGS_API = '/api/inbox_ops/settings'\nconst SCHEDULES_API = '/api/data_sync/schedules'\n\ntype InboxSettings = { id: string; workingLanguage: string; updatedAt: string }\n\nasync function createWebhook(\n request: APIRequestContext,\n token: string,\n stamp: number,\n): Promise<{ id: string; updatedAt: string }> {\n const created = await apiRequest(request, 'POST', WEBHOOKS_API, {\n token,\n data: {\n name: `QA Lock 043 ${stamp}`,\n url: `https://example.com/qa-lock-043-${stamp}`,\n subscribedEvents: ['qa.lock.test'],\n },\n })\n expect(created.status(), 'POST webhook should be 201').toBe(201)\n const body = (await created.json()) as { id?: string }\n expect(typeof body.id, 'webhook creation should return an id').toBe('string')\n const detail = await apiRequest(request, 'GET', `${WEBHOOKS_API}/${body.id}`, { token })\n expect(detail.status(), 'GET webhook detail should be 200').toBe(200)\n const detailBody = (await detail.json()) as { updatedAt?: string }\n expect(typeof detailBody.updatedAt, 'webhook should expose updatedAt').toBe('string')\n return { id: body.id as string, updatedAt: detailBody.updatedAt as string }\n}\n\nasync function readWebhookUpdatedAt(\n request: APIRequestContext,\n token: string,\n id: string,\n): Promise<string> {\n const detail = await apiRequest(request, 'GET', `${WEBHOOKS_API}/${id}`, { token })\n expect(detail.status(), 'GET webhook detail should be 200').toBe(200)\n const body = (await detail.json()) as { updatedAt?: string }\n return body.updatedAt as string\n}\n\nasync function deleteWebhook(\n request: APIRequestContext,\n token: string,\n id: string,\n): Promise<void> {\n const current = await readWebhookUpdatedAt(request, token, id).catch(() => undefined)\n await request\n .fetch(resolveApiUrl(`${WEBHOOKS_API}/${id}`), {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n ...(current ? { [OPTIMISTIC_LOCK_HEADER_NAME]: current } : {}),\n },\n })\n .catch(() => undefined)\n}\n\nasync function patchInboxSettings(\n request: APIRequestContext,\n token: string,\n body: Record<string, unknown>,\n lockValue?: string,\n) {\n return request.fetch(resolveApiUrl(INBOX_SETTINGS_API), {\n method: 'PATCH',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n ...(lockValue !== undefined ? { [OPTIMISTIC_LOCK_HEADER_NAME]: lockValue } : {}),\n },\n data: body,\n })\n}\n\nasync function readInboxSettings(\n request: APIRequestContext,\n token: string,\n): Promise<InboxSettings> {\n const response = await apiRequest(request, 'GET', INBOX_SETTINGS_API, { token })\n expect(response.status(), 'GET inbox settings should be 200').toBe(200)\n const body = (await response.json()) as { settings?: InboxSettings | null }\n expect(body.settings, 'tenant inbox settings singleton should exist').toBeTruthy()\n return body.settings as InboxSettings\n}\n\nasync function postSchedule(\n request: APIRequestContext,\n token: string,\n scheduleValue: string,\n integrationId: string,\n lockValue?: string,\n) {\n return request.fetch(resolveApiUrl(SCHEDULES_API), {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n ...(lockValue !== undefined ? { [OPTIMISTIC_LOCK_HEADER_NAME]: lockValue } : {}),\n },\n data: {\n integrationId,\n entityType: 'products',\n direction: 'import',\n scheduleType: 'cron',\n scheduleValue,\n timezone: 'UTC',\n fullSync: false,\n isEnabled: true,\n },\n })\n}\n\ntest.describe('TC-LOCK-OSS-043: webhooks + inbox settings + data-sync schedule optimistic lock', () => {\n test('WHK-01 stale webhook PUT is refused with a 409 conflict', async ({ page }) => {\n const token = await getAuthToken(page.request, 'admin')\n const stamp = Date.now()\n let webhookId: string | null = null\n try {\n const webhook = await createWebhook(page.request, token, stamp)\n webhookId = webhook.id\n const staleUpdatedAt = webhook.updatedAt\n\n // Advance updated_at out-of-band via a header-less PUT.\n const bump = await apiRequest(page.request, 'PUT', `${WEBHOOKS_API}/${webhookId}`, {\n token,\n data: { name: `QA Lock 043 bumped ${stamp}` },\n })\n expect(bump.status(), 'out-of-band webhook PUT should succeed').toBeLessThan(300)\n\n // Replay the now-stale write \u2192 409.\n const conflict = await putWithLock(\n page.request,\n token,\n `${WEBHOOKS_API}/${webhookId}`,\n { name: `QA Lock 043 stale ${stamp}` },\n staleUpdatedAt,\n )\n await expectConflictBody(conflict)\n } finally {\n if (webhookId) await deleteWebhook(page.request, token, webhookId)\n }\n })\n\n test('WHK-01 stale webhook DELETE is refused with a 409 conflict', async ({ page }) => {\n const token = await getAuthToken(page.request, 'admin')\n const stamp = Date.now()\n let webhookId: string | null = null\n try {\n const webhook = await createWebhook(page.request, token, stamp)\n webhookId = webhook.id\n const staleUpdatedAt = webhook.updatedAt\n\n const bump = await apiRequest(page.request, 'PUT', `${WEBHOOKS_API}/${webhookId}`, {\n token,\n data: { name: `QA Lock 043 bumped ${stamp}` },\n })\n expect(bump.status(), 'out-of-band webhook PUT should succeed').toBeLessThan(300)\n\n const conflict = await page.request.fetch(resolveApiUrl(`${WEBHOOKS_API}/${webhookId}`), {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n [OPTIMISTIC_LOCK_HEADER_NAME]: staleUpdatedAt,\n },\n })\n await expectConflictBody(conflict)\n } finally {\n if (webhookId) await deleteWebhook(page.request, token, webhookId)\n }\n })\n\n // WHK-01 (browser UI): now ACTIVE. The product fix is committed/verified \u2014 the webhooks\n // server DELETE returns the structured 409 (proven by the active \"WHK-01 stale webhook\n // DELETE is refused with a 409\" API test) and the list page sends the lock header\n // (`buildOptimisticLockHeader(row.updatedAt)`) + routes the resulting 409 through\n // surfaceRecordConflict. The RowActions menu opens on CLICK of the \"Open actions\" kebab and\n // renders the items in a portal on document.body (role=\"menuitem\"); the confirm dialog is a\n // role=\"alertdialog\" with a \"Confirm\" button. We drive that choreography robustly here:\n // wait for the filtered list GET to settle, open the kebab, click Delete, confirm, then\n // assert the unified conflict bar (never the success toast).\n test('WHK-01 stale webhook DELETE in the list surfaces the conflict bar', async ({ page }) => {\n const token = await getAuthToken(page.request, 'admin')\n const stamp = Date.now()\n const webhookName = `QA Lock 043 ${stamp}`\n let webhookId: string | null = null\n try {\n const webhook = await createWebhook(page.request, token, stamp)\n webhookId = webhook.id\n\n await login(page, 'admin')\n await page.goto('/backend/webhooks')\n // Wait for the list GET to settle before interacting. The list reflects the\n // freshly created fixture immediately (newest-first), so a unique name makes\n // the row unambiguous.\n await page\n .waitForResponse(\n (response) =>\n response.url().includes('/api/webhooks') &&\n response.request().method() === 'GET' &&\n response.ok(),\n { timeout: 20_000 },\n )\n .catch(() => undefined)\n\n const row = page.getByRole('row', { name: new RegExp(`${webhookName}\\\\b`) }).first()\n await expect(row, 'created webhook should appear in the list').toBeVisible({ timeout: 20_000 })\n\n // Advance updated_at out-of-band \u2192 the in-page row token is now stale.\n // NOTE: bump description (not name) so the row locator stays valid even if\n // the list re-fetches after the PUT.\n const bump = await apiRequest(page.request, 'PUT', `${WEBHOOKS_API}/${webhookId}`, {\n token,\n data: { description: `bumped-${stamp}` },\n })\n expect(bump.status(), 'out-of-band webhook PUT should succeed').toBeLessThan(300)\n\n // Open the row's RowActions kebab (opens on click) and trigger Delete. The\n // menu renders in a portal on document.body, so query it at the page level.\n const kebab = row.getByRole('button', { name: /open actions/i })\n await expect(kebab, 'row should expose an Open actions trigger').toBeVisible()\n await kebab.click()\n const deleteItem = page.getByRole('menuitem', { name: /^delete$/i })\n await expect(deleteItem, 'delete menu item should be visible').toBeVisible({ timeout: 10_000 })\n await deleteItem.click()\n\n // Confirm the destructive alertdialog (the row still holds the stale\n // updated_at captured at render time, so the DELETE 409s).\n const dialog = page.getByRole('alertdialog')\n await expect(dialog, 'confirm dialog should open').toBeVisible({ timeout: 10_000 })\n await dialog.getByRole('button', { name: /^(confirm|delete)$/i }).click()\n\n // The client must route the 409 to the unified conflict bar, never the\n // success toast.\n await expectConflictBanner(page)\n } finally {\n if (webhookId) await deleteWebhook(page.request, token, webhookId)\n }\n })\n\n test('INB-01 stale inbox-settings PATCH is refused with a 409 conflict', async ({ page }) => {\n const token = await getAuthToken(page.request, 'admin')\n const before = await readInboxSettings(page.request, token)\n const originalLanguage = before.workingLanguage\n const staleUpdatedAt = before.updatedAt\n const bumpLanguage = originalLanguage === 'de' ? 'en' : 'de'\n const staleLanguage = originalLanguage === 'es' ? 'pl' : 'es'\n try {\n // Advance updated_at out-of-band via a header-less PATCH.\n const bump = await patchInboxSettings(page.request, token, { workingLanguage: bumpLanguage })\n expect(bump.status(), 'out-of-band inbox PATCH should succeed').toBeLessThan(300)\n\n // Replay the now-stale write with the original expected-version \u2192 409.\n const conflict = await patchInboxSettings(\n page.request,\n token,\n { workingLanguage: staleLanguage },\n staleUpdatedAt,\n )\n await expectConflictBody(conflict)\n } finally {\n // Restore the original working language with a fresh lock token.\n const current = await readInboxSettings(page.request, token).catch(() => null)\n if (current) {\n await patchInboxSettings(\n page.request,\n token,\n { workingLanguage: originalLanguage },\n current.updatedAt,\n ).catch(() => undefined)\n }\n }\n })\n\n test('SYNC-01 stale data-sync schedule save is refused with a 409 conflict', async ({ page }) => {\n const token = await getAuthToken(page.request, 'admin')\n const stamp = Date.now()\n const integrationId = `qa-lock-043-${stamp}`\n let scheduleId: string | null = null\n try {\n const created = await postSchedule(page.request, token, '0 * * * *', integrationId)\n expect(created.status(), 'POST schedule should be 201').toBe(201)\n const createdBody = (await created.json()) as { id?: string; updatedAt?: string }\n scheduleId = createdBody.id ?? null\n const staleUpdatedAt = createdBody.updatedAt\n expect(typeof scheduleId, 'schedule creation should return an id').toBe('string')\n expect(typeof staleUpdatedAt, 'schedule should expose updatedAt').toBe('string')\n\n // Advance updated_at out-of-band via the header-less [id] PUT route.\n const bump = await apiRequest(page.request, 'PUT', `${SCHEDULES_API}/${scheduleId}`, {\n token,\n data: { scheduleValue: '5 * * * *' },\n })\n expect(bump.status(), 'out-of-band schedule PUT should succeed').toBeLessThan(300)\n\n // Replay the save for the same (integrationId, entityType, direction) key\n // with the now-stale expected-version \u2192 409.\n const conflict = await postSchedule(\n page.request,\n token,\n '9 * * * *',\n integrationId,\n staleUpdatedAt as string,\n )\n await expectConflictBody(conflict)\n } finally {\n if (scheduleId) {\n await apiRequest(page.request, 'DELETE', `${SCHEDULES_API}/${scheduleId}`, { token }).catch(\n () => undefined,\n )\n }\n }\n })\n})\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { test, expect, type APIRequestContext } from '@playwright/test'\nimport { getAuthToken, apiRequest } from '@open-mercato/core/modules/core/__integration__/helpers/api'\nimport { login } from '@open-mercato/core/modules/core/__integration__/helpers/auth'\nimport {\n putWithLock,\n expectConflictBody,\n expectConflictBanner,\n resolveApiUrl,\n} from '@open-mercato/core/modules/core/__integration__/helpers/optimisticLockUi'\nimport { OPTIMISTIC_LOCK_HEADER_NAME } from '@open-mercato/shared/lib/crud/optimistic-lock-headers'\n\n/**\n * TC-LOCK-OSS-043 \u2014 webhooks + inbox settings + data-sync schedule\n * optimistic-lock conflict contract (WHK-01, INB-01, SYNC-01).\n *\n * All three surfaces guard writes with `enforceCommandOptimisticLock` inside\n * pure command/API routes \u2014 none expose a dedicated `data-crud-field-id`\n * CrudForm edit page for the locked record, so the conflict contract is proven\n * at the API level: capture the record's `updated_at`, advance it out-of-band\n * via a header-less write (the strictly-additive path always succeeds and bumps\n * `updated_at`), then replay the now-stale write with the original\n * expected-version header \u2192 409 `optimistic_lock_conflict`.\n *\n * - WHK-01: `PUT /api/webhooks/<id>` and `DELETE /api/webhooks/<id>`\n * (`packages/webhooks/src/modules/webhooks/api/webhooks/[id]/route.ts`,\n * `resourceKind: 'webhooks.endpoint'`).\n * - INB-01: `PATCH /api/inbox_ops/settings`\n * (`packages/core/src/modules/inbox_ops/api/settings/route.ts`,\n * `resourceKind: 'inbox_ops.settings'`). The settings row is a per-tenant\n * singleton, so the test bumps + restores `workingLanguage` instead of\n * creating/deleting a fixture.\n * - SYNC-01: `POST /api/data_sync/schedules`\n * (`packages/core/src/modules/data_sync/api/schedules/route.ts` \u2192\n * `SyncScheduleService.saveSchedule`, `resourceKind: 'data_sync.schedule'`).\n * The collection POST reads the expected-version header; the service enforces\n * the lock when a row with the same (integrationId, entityType, direction)\n * key already exists.\n */\n\nconst WEBHOOKS_API = '/api/webhooks'\nconst INBOX_SETTINGS_API = '/api/inbox_ops/settings'\nconst SCHEDULES_API = '/api/data_sync/schedules'\n\ntype InboxSettings = { id: string; workingLanguage: string; updatedAt: string }\n\nasync function createWebhook(\n request: APIRequestContext,\n token: string,\n stamp: number,\n): Promise<{ id: string; updatedAt: string }> {\n const created = await apiRequest(request, 'POST', WEBHOOKS_API, {\n token,\n data: {\n name: `QA Lock 043 ${stamp}`,\n url: `https://example.com/qa-lock-043-${stamp}`,\n subscribedEvents: ['qa.lock.test'],\n },\n })\n expect(created.status(), 'POST webhook should be 201').toBe(201)\n const body = (await created.json()) as { id?: string }\n expect(typeof body.id, 'webhook creation should return an id').toBe('string')\n const detail = await apiRequest(request, 'GET', `${WEBHOOKS_API}/${body.id}`, { token })\n expect(detail.status(), 'GET webhook detail should be 200').toBe(200)\n const detailBody = (await detail.json()) as { updatedAt?: string }\n expect(typeof detailBody.updatedAt, 'webhook should expose updatedAt').toBe('string')\n return { id: body.id as string, updatedAt: detailBody.updatedAt as string }\n}\n\nasync function readWebhookUpdatedAt(\n request: APIRequestContext,\n token: string,\n id: string,\n): Promise<string> {\n const detail = await apiRequest(request, 'GET', `${WEBHOOKS_API}/${id}`, { token })\n expect(detail.status(), 'GET webhook detail should be 200').toBe(200)\n const body = (await detail.json()) as { updatedAt?: string }\n return body.updatedAt as string\n}\n\nasync function deleteWebhook(\n request: APIRequestContext,\n token: string,\n id: string,\n): Promise<void> {\n const current = await readWebhookUpdatedAt(request, token, id).catch(() => undefined)\n await request\n .fetch(resolveApiUrl(`${WEBHOOKS_API}/${id}`), {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n ...(current ? { [OPTIMISTIC_LOCK_HEADER_NAME]: current } : {}),\n },\n })\n .catch(() => undefined)\n}\n\nasync function patchInboxSettings(\n request: APIRequestContext,\n token: string,\n body: Record<string, unknown>,\n lockValue?: string,\n) {\n return request.fetch(resolveApiUrl(INBOX_SETTINGS_API), {\n method: 'PATCH',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n ...(lockValue !== undefined ? { [OPTIMISTIC_LOCK_HEADER_NAME]: lockValue } : {}),\n },\n data: body,\n })\n}\n\nasync function readInboxSettings(\n request: APIRequestContext,\n token: string,\n): Promise<InboxSettings> {\n const response = await apiRequest(request, 'GET', INBOX_SETTINGS_API, { token })\n expect(response.status(), 'GET inbox settings should be 200').toBe(200)\n const body = (await response.json()) as { settings?: InboxSettings | null }\n expect(body.settings, 'tenant inbox settings singleton should exist').toBeTruthy()\n return body.settings as InboxSettings\n}\n\nasync function postSchedule(\n request: APIRequestContext,\n token: string,\n scheduleValue: string,\n integrationId: string,\n lockValue?: string,\n) {\n return request.fetch(resolveApiUrl(SCHEDULES_API), {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n ...(lockValue !== undefined ? { [OPTIMISTIC_LOCK_HEADER_NAME]: lockValue } : {}),\n },\n data: {\n integrationId,\n entityType: 'products',\n direction: 'import',\n scheduleType: 'cron',\n scheduleValue,\n timezone: 'UTC',\n fullSync: false,\n isEnabled: true,\n },\n })\n}\n\ntest.describe('TC-LOCK-OSS-043: webhooks + inbox settings + data-sync schedule optimistic lock', () => {\n test('WHK-01 stale webhook PUT is refused with a 409 conflict', async ({ page }) => {\n const token = await getAuthToken(page.request, 'admin')\n const stamp = Date.now()\n let webhookId: string | null = null\n try {\n const webhook = await createWebhook(page.request, token, stamp)\n webhookId = webhook.id\n const staleUpdatedAt = webhook.updatedAt\n\n // Advance updated_at out-of-band via a header-less PUT.\n const bump = await apiRequest(page.request, 'PUT', `${WEBHOOKS_API}/${webhookId}`, {\n token,\n data: { name: `QA Lock 043 bumped ${stamp}` },\n })\n expect(bump.status(), 'out-of-band webhook PUT should succeed').toBeLessThan(300)\n\n // Replay the now-stale write \u2192 409.\n const conflict = await putWithLock(\n page.request,\n token,\n `${WEBHOOKS_API}/${webhookId}`,\n { name: `QA Lock 043 stale ${stamp}` },\n staleUpdatedAt,\n )\n await expectConflictBody(conflict)\n } finally {\n if (webhookId) await deleteWebhook(page.request, token, webhookId)\n }\n })\n\n test('WHK-01 stale webhook DELETE is refused with a 409 conflict', async ({ page }) => {\n const token = await getAuthToken(page.request, 'admin')\n const stamp = Date.now()\n let webhookId: string | null = null\n try {\n const webhook = await createWebhook(page.request, token, stamp)\n webhookId = webhook.id\n const staleUpdatedAt = webhook.updatedAt\n\n const bump = await apiRequest(page.request, 'PUT', `${WEBHOOKS_API}/${webhookId}`, {\n token,\n data: { name: `QA Lock 043 bumped ${stamp}` },\n })\n expect(bump.status(), 'out-of-band webhook PUT should succeed').toBeLessThan(300)\n\n const conflict = await page.request.fetch(resolveApiUrl(`${WEBHOOKS_API}/${webhookId}`), {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n [OPTIMISTIC_LOCK_HEADER_NAME]: staleUpdatedAt,\n },\n })\n await expectConflictBody(conflict)\n } finally {\n if (webhookId) await deleteWebhook(page.request, token, webhookId)\n }\n })\n\n // WHK-01 (browser UI): now ACTIVE. The product fix is committed/verified \u2014 the webhooks\n // server DELETE returns the structured 409 (proven by the active \"WHK-01 stale webhook\n // DELETE is refused with a 409\" API test) and the list page sends the lock header\n // (`buildOptimisticLockHeader(row.updatedAt)`) + routes the resulting 409 through\n // surfaceRecordConflict. The RowActions menu opens on CLICK of the \"Open actions\" kebab and\n // renders the items in a portal on document.body (role=\"menuitem\"); the confirm dialog is a\n // role=\"alertdialog\" with a \"Confirm\" button. We drive that choreography robustly here:\n // wait for the filtered list GET to settle, open the kebab, click Delete, confirm, then\n // assert the unified conflict bar (never the success toast).\n test('WHK-01 stale webhook DELETE in the list surfaces the conflict bar', async ({ page }) => {\n // Heavy browser flow (login + list load + portalled RowActions menu) routinely\n // exceeds Playwright's 20s default on a loaded ephemeral shard; opt into the\n // sanctioned per-test budget (see TC-LOCK-OSS-029). Global bump is disallowed.\n test.setTimeout(60_000)\n const token = await getAuthToken(page.request, 'admin')\n const stamp = Date.now()\n let webhookId: string | null = null\n try {\n const webhook = await createWebhook(page.request, token, stamp)\n webhookId = webhook.id\n\n await login(page, 'admin')\n await page.goto('/backend/webhooks')\n // Wait for the list GET to settle before interacting. The list reflects the\n // freshly created fixture immediately (newest-first), so a unique name makes\n // the row unambiguous.\n await page\n .waitForResponse(\n (response) =>\n response.url().includes('/api/webhooks') &&\n response.request().method() === 'GET' &&\n response.ok(),\n { timeout: 20_000 },\n )\n .catch(() => undefined)\n\n // The out-of-band PUT below renames the fixture to \"QA Lock 043 bumped <stamp>\";\n // tolerate both names so a mid-test list re-render cannot orphan the row locator.\n const row = page\n .getByRole('row', { name: new RegExp(`QA Lock 043 (?:bumped )?${stamp}\\\\b`) })\n .first()\n await expect(row, 'created webhook should appear in the list').toBeVisible({ timeout: 20_000 })\n\n // Advance updated_at out-of-band \u2192 the in-page row token is now stale.\n // NOTE: bump description (not name) so the row locator stays valid even if\n // the list re-fetches after the PUT.\n const bump = await apiRequest(page.request, 'PUT', `${WEBHOOKS_API}/${webhookId}`, {\n token,\n data: { description: `bumped-${stamp}` },\n })\n expect(bump.status(), 'out-of-band webhook PUT should succeed').toBeLessThan(300)\n\n // Open the row's RowActions kebab (opens on click) and trigger Delete. The\n // menu renders in a portal on document.body, so query it at the page level.\n // The list re-renders as data settles, so the menu can detach between \"open\"\n // and \"click\" \u2014 retry (re)open-menu + click-Delete atomically (same fix as\n // TC-LOCK-OSS-029); the confirm dialog gates the DELETE, so a repeated\n // click is safe.\n const kebab = row.getByRole('button', { name: /open actions/i })\n const deleteItem = page.getByRole('menuitem', { name: /^delete$/i })\n await expect(async () => {\n if (!(await deleteItem.isVisible().catch(() => false))) {\n await kebab.click({ timeout: 2_000 }).catch(() => {})\n await expect(deleteItem).toBeVisible({ timeout: 1_500 })\n }\n await deleteItem.click({ timeout: 2_000 })\n }).toPass({ timeout: 30_000 })\n\n // Confirm the destructive alertdialog (the row still holds the stale\n // updated_at captured at render time, so the DELETE 409s).\n const dialog = page.getByRole('alertdialog')\n await expect(dialog, 'confirm dialog should open').toBeVisible({ timeout: 10_000 })\n await dialog.getByRole('button', { name: /^(confirm|delete)$/i }).click()\n\n // The client must route the 409 to the unified conflict bar, never the\n // success toast.\n await expectConflictBanner(page)\n } finally {\n if (webhookId) await deleteWebhook(page.request, token, webhookId)\n }\n })\n\n test('INB-01 stale inbox-settings PATCH is refused with a 409 conflict', async ({ page }) => {\n const token = await getAuthToken(page.request, 'admin')\n const before = await readInboxSettings(page.request, token)\n const originalLanguage = before.workingLanguage\n const staleUpdatedAt = before.updatedAt\n const bumpLanguage = originalLanguage === 'de' ? 'en' : 'de'\n const staleLanguage = originalLanguage === 'es' ? 'pl' : 'es'\n try {\n // Advance updated_at out-of-band via a header-less PATCH.\n const bump = await patchInboxSettings(page.request, token, { workingLanguage: bumpLanguage })\n expect(bump.status(), 'out-of-band inbox PATCH should succeed').toBeLessThan(300)\n\n // Replay the now-stale write with the original expected-version \u2192 409.\n const conflict = await patchInboxSettings(\n page.request,\n token,\n { workingLanguage: staleLanguage },\n staleUpdatedAt,\n )\n await expectConflictBody(conflict)\n } finally {\n // Restore the original working language with a fresh lock token.\n const current = await readInboxSettings(page.request, token).catch(() => null)\n if (current) {\n await patchInboxSettings(\n page.request,\n token,\n { workingLanguage: originalLanguage },\n current.updatedAt,\n ).catch(() => undefined)\n }\n }\n })\n\n test('SYNC-01 stale data-sync schedule save is refused with a 409 conflict', async ({ page }) => {\n const token = await getAuthToken(page.request, 'admin')\n const stamp = Date.now()\n const integrationId = `qa-lock-043-${stamp}`\n let scheduleId: string | null = null\n try {\n const created = await postSchedule(page.request, token, '0 * * * *', integrationId)\n expect(created.status(), 'POST schedule should be 201').toBe(201)\n const createdBody = (await created.json()) as { id?: string; updatedAt?: string }\n scheduleId = createdBody.id ?? null\n const staleUpdatedAt = createdBody.updatedAt\n expect(typeof scheduleId, 'schedule creation should return an id').toBe('string')\n expect(typeof staleUpdatedAt, 'schedule should expose updatedAt').toBe('string')\n\n // Advance updated_at out-of-band via the header-less [id] PUT route.\n const bump = await apiRequest(page.request, 'PUT', `${SCHEDULES_API}/${scheduleId}`, {\n token,\n data: { scheduleValue: '5 * * * *' },\n })\n expect(bump.status(), 'out-of-band schedule PUT should succeed').toBeLessThan(300)\n\n // Replay the save for the same (integrationId, entityType, direction) key\n // with the now-stale expected-version \u2192 409.\n const conflict = await postSchedule(\n page.request,\n token,\n '9 * * * *',\n integrationId,\n staleUpdatedAt as string,\n )\n await expectConflictBody(conflict)\n } finally {\n if (scheduleId) {\n await apiRequest(page.request, 'DELETE', `${SCHEDULES_API}/${scheduleId}`, { token }).catch(\n () => undefined,\n )\n }\n }\n })\n})\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -110,8 +110,7 @@ function IntegrationSetupWidget({ context }) {
110
110
  "webhooks.integrationSetup.summary",
111
111
  "Manage Custom Webhooks from the dedicated webhook screen. Turning this integration off blocks outbound deliveries, test sends, retries, and inbound webhook receives."
112
112
  ) }),
113
- /* @__PURE__ */ jsxs(Alert, { children: [
114
- /* @__PURE__ */ jsx(Webhook, { className: "h-4 w-4" }),
113
+ /* @__PURE__ */ jsxs(Alert, { icon: /* @__PURE__ */ jsx(Webhook, { "aria-hidden": "true" }), children: [
115
114
  /* @__PURE__ */ jsx(AlertTitle, { children: isEnabled ? t("webhooks.integrationSetup.enabledTitle", "Delivery processing is enabled") : t("webhooks.integrationSetup.disabledTitle", "Delivery processing is disabled") }),
116
115
  /* @__PURE__ */ jsx(AlertDescription, { children: isEnabled ? t("webhooks.integrationSetup.enabledBody", "Your saved webhooks can send and receive traffic. Use the links below to review endpoints and open the delivery log on each webhook detail page.") : t("webhooks.integrationSetup.disabledBody", "The integration switch is off, so webhook sends, retries, test deliveries, and inbound receives are blocked until you enable it again.") })
117
116
  ] }),
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/webhooks/widgets/injection/integration-setup/widget.client.tsx"],
4
- "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { ExternalLink, PencilLine, Plus, Webhook } from 'lucide-react'\nimport type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { Switch } from '@open-mercato/ui/primitives/switch'\nimport { webhookCustomIntegrationId } from '../../../integration'\n\ntype WebhookIntegrationContext = {\n state?: {\n isEnabled?: boolean\n } | null\n}\n\ntype WebhookListItem = {\n id: string\n name: string\n url: string\n isActive: boolean\n subscribedEvents: string[]\n}\n\ntype WebhookListResponse = {\n items: WebhookListItem[]\n total: number\n}\n\ntype WebhookIntegrationCredentials = Record<string, string | number | boolean | null> & {\n notifyOnFailedDelivery?: boolean\n}\n\ntype WebhookCredentialsResponse = {\n credentials: WebhookIntegrationCredentials\n}\n\nexport default function IntegrationSetupWidget({ context }: InjectionWidgetComponentProps) {\n const t = useT()\n const typedContext = context as WebhookIntegrationContext | undefined\n const [items, setItems] = React.useState<WebhookListItem[]>([])\n const [total, setTotal] = React.useState(0)\n const [isLoading, setIsLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [credentials, setCredentials] = React.useState<WebhookIntegrationCredentials>({})\n const [settingsError, setSettingsError] = React.useState<string | null>(null)\n const [isSavingSettings, setIsSavingSettings] = React.useState(false)\n\n React.useEffect(() => {\n let mounted = true\n\n const load = async () => {\n setIsLoading(true)\n setError(null)\n\n try {\n const [listCall, credentialsCall] = await Promise.all([\n apiCall<WebhookListResponse>(\n '/api/webhooks?page=1&pageSize=6',\n undefined,\n { fallback: { items: [], total: 0 } },\n ),\n apiCall<WebhookCredentialsResponse>(\n `/api/integrations/${encodeURIComponent(webhookCustomIntegrationId)}/credentials`,\n undefined,\n { fallback: null },\n ),\n ])\n\n if (!mounted) return\n\n if (!listCall.ok || !listCall.result) {\n setError(t('webhooks.integrationSetup.loadError', 'Failed to load configured webhooks.'))\n setItems([])\n setTotal(0)\n } else {\n setItems(listCall.result.items)\n setTotal(listCall.result.total)\n }\n\n if (credentialsCall.ok && credentialsCall.result?.credentials) {\n setCredentials(credentialsCall.result.credentials)\n setSettingsError(null)\n } else {\n setSettingsError(t('webhooks.integrationSetup.settingsLoadError', 'Failed to load webhook integration settings.'))\n }\n } catch {\n if (!mounted) return\n setError(t('webhooks.integrationSetup.loadError', 'Failed to load configured webhooks.'))\n setItems([])\n setTotal(0)\n setSettingsError(t('webhooks.integrationSetup.settingsLoadError', 'Failed to load webhook integration settings.'))\n } finally {\n if (mounted) setIsLoading(false)\n }\n }\n\n void load()\n\n return () => {\n mounted = false\n }\n }, [t])\n\n const isEnabled = typedContext?.state?.isEnabled !== false\n const notifyOnFailedDelivery = credentials.notifyOnFailedDelivery === true\n\n const handleToggleFailedDeliveryNotifications = React.useCallback(async (nextValue: boolean) => {\n const nextCredentials: WebhookIntegrationCredentials = {\n ...credentials,\n notifyOnFailedDelivery: nextValue,\n }\n\n setCredentials(nextCredentials)\n setIsSavingSettings(true)\n setSettingsError(null)\n\n try {\n const call = await apiCall(\n `/api/integrations/${encodeURIComponent(webhookCustomIntegrationId)}/credentials`,\n {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ credentials: nextCredentials }),\n },\n { fallback: null },\n )\n\n if (!call.ok) {\n setCredentials(credentials)\n setSettingsError(t('webhooks.integrationSetup.settingsSaveError', 'Failed to save webhook integration settings.'))\n flash(t('webhooks.integrationSetup.settingsSaveError', 'Failed to save webhook integration settings.'), 'error')\n return\n }\n\n flash(t('webhooks.integrationSetup.settingsSaveSuccess', 'Webhook integration settings saved.'), 'success')\n } catch {\n setCredentials(credentials)\n setSettingsError(t('webhooks.integrationSetup.settingsSaveError', 'Failed to save webhook integration settings.'))\n flash(t('webhooks.integrationSetup.settingsSaveError', 'Failed to save webhook integration settings.'), 'error')\n } finally {\n setIsSavingSettings(false)\n }\n }, [credentials, t])\n\n return (\n <div className=\"space-y-4\">\n <p className=\"text-sm text-muted-foreground\">\n {t(\n 'webhooks.integrationSetup.summary',\n 'Manage Custom Webhooks from the dedicated webhook screen. Turning this integration off blocks outbound deliveries, test sends, retries, and inbound webhook receives.',\n )}\n </p>\n\n <Alert>\n <Webhook className=\"h-4 w-4\" />\n <AlertTitle>\n {isEnabled\n ? t('webhooks.integrationSetup.enabledTitle', 'Delivery processing is enabled')\n : t('webhooks.integrationSetup.disabledTitle', 'Delivery processing is disabled')}\n </AlertTitle>\n <AlertDescription>\n {isEnabled\n ? t('webhooks.integrationSetup.enabledBody', 'Your saved webhooks can send and receive traffic. Use the links below to review endpoints and open the delivery log on each webhook detail page.')\n : t('webhooks.integrationSetup.disabledBody', 'The integration switch is off, so webhook sends, retries, test deliveries, and inbound receives are blocked until you enable it again.')}\n </AlertDescription>\n </Alert>\n\n <div className=\"flex flex-wrap gap-3\">\n <Button asChild>\n <Link href=\"/backend/webhooks\">\n <ExternalLink className=\"mr-2 h-4 w-4\" />\n {t('webhooks.integrationSetup.actions.openList', 'Open webhook list')}\n </Link>\n </Button>\n <Button asChild variant=\"outline\">\n <Link href=\"/backend/webhooks/create\">\n <Plus className=\"mr-2 h-4 w-4\" />\n {t('webhooks.integrationSetup.actions.create', 'Create webhook')}\n </Link>\n </Button>\n </div>\n\n <div className=\"rounded-lg border bg-muted/30 p-4\">\n <div className=\"flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between\">\n <div className=\"space-y-1\">\n <h4 className=\"text-sm font-semibold\">\n {t('webhooks.integrationSetup.notificationsTitle', 'Failed delivery notifications')}\n </h4>\n <p className=\"text-sm text-muted-foreground\">\n {t(\n 'webhooks.integrationSetup.notificationsBody',\n 'Notify admin users when a webhook delivery finally fails after retries are exhausted.',\n )}\n </p>\n </div>\n <div className=\"flex items-center gap-3\">\n <Badge variant={notifyOnFailedDelivery ? 'default' : 'outline'}>\n {notifyOnFailedDelivery\n ? t('webhooks.integrationSetup.notificationsEnabled', 'Enabled')\n : t('webhooks.integrationSetup.notificationsDisabled', 'Disabled')}\n </Badge>\n <Switch\n checked={notifyOnFailedDelivery}\n disabled={isSavingSettings}\n onCheckedChange={(checked) => { void handleToggleFailedDeliveryNotifications(checked) }}\n />\n </div>\n </div>\n {settingsError ? (\n <div className=\"mt-3 rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-sm text-destructive\">\n {settingsError}\n </div>\n ) : null}\n </div>\n\n <div className=\"space-y-3\">\n <div className=\"flex items-center justify-between gap-3\">\n <h4 className=\"text-sm font-semibold\">\n {t('webhooks.integrationSetup.configuredTitle', 'Configured webhooks')}\n </h4>\n {total > items.length ? (\n <span className=\"text-xs text-muted-foreground\">\n {t('webhooks.integrationSetup.more', 'Showing {shown} of {total}.', {\n shown: String(items.length),\n total: String(total),\n })}\n </span>\n ) : null}\n </div>\n\n {isLoading ? (\n <div className=\"flex items-center gap-2 rounded-md border bg-muted/30 px-3 py-3 text-sm text-muted-foreground\">\n <Spinner className=\"h-4 w-4\" />\n <span>{t('webhooks.integrationSetup.loading', 'Loading configured webhooks...')}</span>\n </div>\n ) : null}\n\n {!isLoading && error ? (\n <div className=\"rounded-md border border-destructive/30 bg-destructive/5 px-3 py-3 text-sm text-destructive\">\n {error}\n </div>\n ) : null}\n\n {!isLoading && !error && items.length === 0 ? (\n <div className=\"rounded-md border border-dashed px-3 py-4 text-sm text-muted-foreground\">\n {t('webhooks.integrationSetup.empty', 'No webhooks have been configured yet. Create one to start sending or receiving events.')}\n </div>\n ) : null}\n\n {!isLoading && !error && items.length > 0 ? (\n <div className=\"space-y-2\">\n {items.map((item) => (\n <div\n key={item.id}\n className=\"flex flex-col gap-3 rounded-lg border bg-muted/30 px-3 py-3 lg:flex-row lg:items-center lg:justify-between\"\n >\n <div className=\"min-w-0 space-y-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"font-medium text-foreground\">{item.name}</span>\n <Badge variant={item.isActive ? 'default' : 'outline'}>\n {item.isActive\n ? t('webhooks.list.status.active', 'Active')\n : t('webhooks.list.status.inactive', 'Inactive')}\n </Badge>\n <span className=\"text-xs text-muted-foreground\">\n {t('webhooks.integrationSetup.eventCount', '{count} event patterns', {\n count: String(item.subscribedEvents.length),\n })}\n </span>\n </div>\n <code className=\"block truncate text-xs text-muted-foreground\">{item.url}</code>\n </div>\n\n <Button asChild variant=\"ghost\" size=\"sm\" className=\"w-fit\">\n <Link href={`/backend/webhooks/${encodeURIComponent(item.id)}`}>\n <PencilLine className=\"mr-2 h-4 w-4\" />\n {t('webhooks.integrationSetup.actions.edit', 'Edit webhook')}\n </Link>\n </Button>\n </div>\n ))}\n </div>\n ) : null}\n </div>\n </div>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport { ExternalLink, PencilLine, Plus, Webhook } from 'lucide-react'\nimport type { InjectionWidgetComponentProps } from '@open-mercato/shared/modules/widgets/injection'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { flash } from '@open-mercato/ui/backend/FlashMessages'\nimport { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert'\nimport { Badge } from '@open-mercato/ui/primitives/badge'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { Switch } from '@open-mercato/ui/primitives/switch'\nimport { webhookCustomIntegrationId } from '../../../integration'\n\ntype WebhookIntegrationContext = {\n state?: {\n isEnabled?: boolean\n } | null\n}\n\ntype WebhookListItem = {\n id: string\n name: string\n url: string\n isActive: boolean\n subscribedEvents: string[]\n}\n\ntype WebhookListResponse = {\n items: WebhookListItem[]\n total: number\n}\n\ntype WebhookIntegrationCredentials = Record<string, string | number | boolean | null> & {\n notifyOnFailedDelivery?: boolean\n}\n\ntype WebhookCredentialsResponse = {\n credentials: WebhookIntegrationCredentials\n}\n\nexport default function IntegrationSetupWidget({ context }: InjectionWidgetComponentProps) {\n const t = useT()\n const typedContext = context as WebhookIntegrationContext | undefined\n const [items, setItems] = React.useState<WebhookListItem[]>([])\n const [total, setTotal] = React.useState(0)\n const [isLoading, setIsLoading] = React.useState(true)\n const [error, setError] = React.useState<string | null>(null)\n const [credentials, setCredentials] = React.useState<WebhookIntegrationCredentials>({})\n const [settingsError, setSettingsError] = React.useState<string | null>(null)\n const [isSavingSettings, setIsSavingSettings] = React.useState(false)\n\n React.useEffect(() => {\n let mounted = true\n\n const load = async () => {\n setIsLoading(true)\n setError(null)\n\n try {\n const [listCall, credentialsCall] = await Promise.all([\n apiCall<WebhookListResponse>(\n '/api/webhooks?page=1&pageSize=6',\n undefined,\n { fallback: { items: [], total: 0 } },\n ),\n apiCall<WebhookCredentialsResponse>(\n `/api/integrations/${encodeURIComponent(webhookCustomIntegrationId)}/credentials`,\n undefined,\n { fallback: null },\n ),\n ])\n\n if (!mounted) return\n\n if (!listCall.ok || !listCall.result) {\n setError(t('webhooks.integrationSetup.loadError', 'Failed to load configured webhooks.'))\n setItems([])\n setTotal(0)\n } else {\n setItems(listCall.result.items)\n setTotal(listCall.result.total)\n }\n\n if (credentialsCall.ok && credentialsCall.result?.credentials) {\n setCredentials(credentialsCall.result.credentials)\n setSettingsError(null)\n } else {\n setSettingsError(t('webhooks.integrationSetup.settingsLoadError', 'Failed to load webhook integration settings.'))\n }\n } catch {\n if (!mounted) return\n setError(t('webhooks.integrationSetup.loadError', 'Failed to load configured webhooks.'))\n setItems([])\n setTotal(0)\n setSettingsError(t('webhooks.integrationSetup.settingsLoadError', 'Failed to load webhook integration settings.'))\n } finally {\n if (mounted) setIsLoading(false)\n }\n }\n\n void load()\n\n return () => {\n mounted = false\n }\n }, [t])\n\n const isEnabled = typedContext?.state?.isEnabled !== false\n const notifyOnFailedDelivery = credentials.notifyOnFailedDelivery === true\n\n const handleToggleFailedDeliveryNotifications = React.useCallback(async (nextValue: boolean) => {\n const nextCredentials: WebhookIntegrationCredentials = {\n ...credentials,\n notifyOnFailedDelivery: nextValue,\n }\n\n setCredentials(nextCredentials)\n setIsSavingSettings(true)\n setSettingsError(null)\n\n try {\n const call = await apiCall(\n `/api/integrations/${encodeURIComponent(webhookCustomIntegrationId)}/credentials`,\n {\n method: 'PUT',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ credentials: nextCredentials }),\n },\n { fallback: null },\n )\n\n if (!call.ok) {\n setCredentials(credentials)\n setSettingsError(t('webhooks.integrationSetup.settingsSaveError', 'Failed to save webhook integration settings.'))\n flash(t('webhooks.integrationSetup.settingsSaveError', 'Failed to save webhook integration settings.'), 'error')\n return\n }\n\n flash(t('webhooks.integrationSetup.settingsSaveSuccess', 'Webhook integration settings saved.'), 'success')\n } catch {\n setCredentials(credentials)\n setSettingsError(t('webhooks.integrationSetup.settingsSaveError', 'Failed to save webhook integration settings.'))\n flash(t('webhooks.integrationSetup.settingsSaveError', 'Failed to save webhook integration settings.'), 'error')\n } finally {\n setIsSavingSettings(false)\n }\n }, [credentials, t])\n\n return (\n <div className=\"space-y-4\">\n <p className=\"text-sm text-muted-foreground\">\n {t(\n 'webhooks.integrationSetup.summary',\n 'Manage Custom Webhooks from the dedicated webhook screen. Turning this integration off blocks outbound deliveries, test sends, retries, and inbound webhook receives.',\n )}\n </p>\n\n <Alert icon={<Webhook aria-hidden=\"true\" />}>\n <AlertTitle>\n {isEnabled\n ? t('webhooks.integrationSetup.enabledTitle', 'Delivery processing is enabled')\n : t('webhooks.integrationSetup.disabledTitle', 'Delivery processing is disabled')}\n </AlertTitle>\n <AlertDescription>\n {isEnabled\n ? t('webhooks.integrationSetup.enabledBody', 'Your saved webhooks can send and receive traffic. Use the links below to review endpoints and open the delivery log on each webhook detail page.')\n : t('webhooks.integrationSetup.disabledBody', 'The integration switch is off, so webhook sends, retries, test deliveries, and inbound receives are blocked until you enable it again.')}\n </AlertDescription>\n </Alert>\n\n <div className=\"flex flex-wrap gap-3\">\n <Button asChild>\n <Link href=\"/backend/webhooks\">\n <ExternalLink className=\"mr-2 h-4 w-4\" />\n {t('webhooks.integrationSetup.actions.openList', 'Open webhook list')}\n </Link>\n </Button>\n <Button asChild variant=\"outline\">\n <Link href=\"/backend/webhooks/create\">\n <Plus className=\"mr-2 h-4 w-4\" />\n {t('webhooks.integrationSetup.actions.create', 'Create webhook')}\n </Link>\n </Button>\n </div>\n\n <div className=\"rounded-lg border bg-muted/30 p-4\">\n <div className=\"flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between\">\n <div className=\"space-y-1\">\n <h4 className=\"text-sm font-semibold\">\n {t('webhooks.integrationSetup.notificationsTitle', 'Failed delivery notifications')}\n </h4>\n <p className=\"text-sm text-muted-foreground\">\n {t(\n 'webhooks.integrationSetup.notificationsBody',\n 'Notify admin users when a webhook delivery finally fails after retries are exhausted.',\n )}\n </p>\n </div>\n <div className=\"flex items-center gap-3\">\n <Badge variant={notifyOnFailedDelivery ? 'default' : 'outline'}>\n {notifyOnFailedDelivery\n ? t('webhooks.integrationSetup.notificationsEnabled', 'Enabled')\n : t('webhooks.integrationSetup.notificationsDisabled', 'Disabled')}\n </Badge>\n <Switch\n checked={notifyOnFailedDelivery}\n disabled={isSavingSettings}\n onCheckedChange={(checked) => { void handleToggleFailedDeliveryNotifications(checked) }}\n />\n </div>\n </div>\n {settingsError ? (\n <div className=\"mt-3 rounded-md border border-destructive/30 bg-destructive/5 px-3 py-2 text-sm text-destructive\">\n {settingsError}\n </div>\n ) : null}\n </div>\n\n <div className=\"space-y-3\">\n <div className=\"flex items-center justify-between gap-3\">\n <h4 className=\"text-sm font-semibold\">\n {t('webhooks.integrationSetup.configuredTitle', 'Configured webhooks')}\n </h4>\n {total > items.length ? (\n <span className=\"text-xs text-muted-foreground\">\n {t('webhooks.integrationSetup.more', 'Showing {shown} of {total}.', {\n shown: String(items.length),\n total: String(total),\n })}\n </span>\n ) : null}\n </div>\n\n {isLoading ? (\n <div className=\"flex items-center gap-2 rounded-md border bg-muted/30 px-3 py-3 text-sm text-muted-foreground\">\n <Spinner className=\"h-4 w-4\" />\n <span>{t('webhooks.integrationSetup.loading', 'Loading configured webhooks...')}</span>\n </div>\n ) : null}\n\n {!isLoading && error ? (\n <div className=\"rounded-md border border-destructive/30 bg-destructive/5 px-3 py-3 text-sm text-destructive\">\n {error}\n </div>\n ) : null}\n\n {!isLoading && !error && items.length === 0 ? (\n <div className=\"rounded-md border border-dashed px-3 py-4 text-sm text-muted-foreground\">\n {t('webhooks.integrationSetup.empty', 'No webhooks have been configured yet. Create one to start sending or receiving events.')}\n </div>\n ) : null}\n\n {!isLoading && !error && items.length > 0 ? (\n <div className=\"space-y-2\">\n {items.map((item) => (\n <div\n key={item.id}\n className=\"flex flex-col gap-3 rounded-lg border bg-muted/30 px-3 py-3 lg:flex-row lg:items-center lg:justify-between\"\n >\n <div className=\"min-w-0 space-y-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"font-medium text-foreground\">{item.name}</span>\n <Badge variant={item.isActive ? 'default' : 'outline'}>\n {item.isActive\n ? t('webhooks.list.status.active', 'Active')\n : t('webhooks.list.status.inactive', 'Inactive')}\n </Badge>\n <span className=\"text-xs text-muted-foreground\">\n {t('webhooks.integrationSetup.eventCount', '{count} event patterns', {\n count: String(item.subscribedEvents.length),\n })}\n </span>\n </div>\n <code className=\"block truncate text-xs text-muted-foreground\">{item.url}</code>\n </div>\n\n <Button asChild variant=\"ghost\" size=\"sm\" className=\"w-fit\">\n <Link href={`/backend/webhooks/${encodeURIComponent(item.id)}`}>\n <PencilLine className=\"mr-2 h-4 w-4\" />\n {t('webhooks.integrationSetup.actions.edit', 'Edit webhook')}\n </Link>\n </Button>\n </div>\n ))}\n </div>\n ) : null}\n </div>\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/webhooks",
3
- "version": "0.6.5-develop.5382.1.f542de69af",
3
+ "version": "0.6.6-develop.5412.1.e2a52b14f0",
4
4
  "description": "Webhooks module for Open Mercato — Standard Webhooks compliant outbound/inbound delivery",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -69,19 +69,19 @@
69
69
  }
70
70
  },
71
71
  "dependencies": {
72
- "@open-mercato/core": "0.6.5-develop.5382.1.f542de69af",
73
- "@open-mercato/queue": "0.6.5-develop.5382.1.f542de69af",
74
- "@open-mercato/ui": "0.6.5-develop.5382.1.f542de69af",
72
+ "@open-mercato/core": "0.6.6-develop.5412.1.e2a52b14f0",
73
+ "@open-mercato/queue": "0.6.6-develop.5412.1.e2a52b14f0",
74
+ "@open-mercato/ui": "0.6.6-develop.5412.1.e2a52b14f0",
75
75
  "svix": "^1.95.2"
76
76
  },
77
77
  "peerDependencies": {
78
78
  "@mikro-orm/postgresql": "^7.0.14",
79
- "@open-mercato/shared": "0.6.5-develop.5382.1.f542de69af",
79
+ "@open-mercato/shared": "0.6.6-develop.5412.1.e2a52b14f0",
80
80
  "react": "^19.0.0",
81
81
  "react-dom": "^19.0.0"
82
82
  },
83
83
  "devDependencies": {
84
- "@open-mercato/shared": "0.6.5-develop.5382.1.f542de69af",
84
+ "@open-mercato/shared": "0.6.6-develop.5412.1.e2a52b14f0",
85
85
  "@types/jest": "^30.0.0",
86
86
  "@types/react": "^19.2.17",
87
87
  "@types/react-dom": "^19.2.3",
@@ -100,5 +100,5 @@
100
100
  "url": "https://github.com/open-mercato/open-mercato",
101
101
  "directory": "packages/webhooks"
102
102
  },
103
- "stableVersion": "0.6.4"
103
+ "stableVersion": "0.6.5"
104
104
  }
@@ -220,9 +220,12 @@ test.describe('TC-LOCK-OSS-043: webhooks + inbox settings + data-sync schedule o
220
220
  // wait for the filtered list GET to settle, open the kebab, click Delete, confirm, then
221
221
  // assert the unified conflict bar (never the success toast).
222
222
  test('WHK-01 stale webhook DELETE in the list surfaces the conflict bar', async ({ page }) => {
223
+ // Heavy browser flow (login + list load + portalled RowActions menu) routinely
224
+ // exceeds Playwright's 20s default on a loaded ephemeral shard; opt into the
225
+ // sanctioned per-test budget (see TC-LOCK-OSS-029). Global bump is disallowed.
226
+ test.setTimeout(60_000)
223
227
  const token = await getAuthToken(page.request, 'admin')
224
228
  const stamp = Date.now()
225
- const webhookName = `QA Lock 043 ${stamp}`
226
229
  let webhookId: string | null = null
227
230
  try {
228
231
  const webhook = await createWebhook(page.request, token, stamp)
@@ -243,7 +246,11 @@ test.describe('TC-LOCK-OSS-043: webhooks + inbox settings + data-sync schedule o
243
246
  )
244
247
  .catch(() => undefined)
245
248
 
246
- const row = page.getByRole('row', { name: new RegExp(`${webhookName}\\b`) }).first()
249
+ // The out-of-band PUT below renames the fixture to "QA Lock 043 bumped <stamp>";
250
+ // tolerate both names so a mid-test list re-render cannot orphan the row locator.
251
+ const row = page
252
+ .getByRole('row', { name: new RegExp(`QA Lock 043 (?:bumped )?${stamp}\\b`) })
253
+ .first()
247
254
  await expect(row, 'created webhook should appear in the list').toBeVisible({ timeout: 20_000 })
248
255
 
249
256
  // Advance updated_at out-of-band → the in-page row token is now stale.
@@ -257,12 +264,19 @@ test.describe('TC-LOCK-OSS-043: webhooks + inbox settings + data-sync schedule o
257
264
 
258
265
  // Open the row's RowActions kebab (opens on click) and trigger Delete. The
259
266
  // menu renders in a portal on document.body, so query it at the page level.
267
+ // The list re-renders as data settles, so the menu can detach between "open"
268
+ // and "click" — retry (re)open-menu + click-Delete atomically (same fix as
269
+ // TC-LOCK-OSS-029); the confirm dialog gates the DELETE, so a repeated
270
+ // click is safe.
260
271
  const kebab = row.getByRole('button', { name: /open actions/i })
261
- await expect(kebab, 'row should expose an Open actions trigger').toBeVisible()
262
- await kebab.click()
263
272
  const deleteItem = page.getByRole('menuitem', { name: /^delete$/i })
264
- await expect(deleteItem, 'delete menu item should be visible').toBeVisible({ timeout: 10_000 })
265
- await deleteItem.click()
273
+ await expect(async () => {
274
+ if (!(await deleteItem.isVisible().catch(() => false))) {
275
+ await kebab.click({ timeout: 2_000 }).catch(() => {})
276
+ await expect(deleteItem).toBeVisible({ timeout: 1_500 })
277
+ }
278
+ await deleteItem.click({ timeout: 2_000 })
279
+ }).toPass({ timeout: 30_000 })
266
280
 
267
281
  // Confirm the destructive alertdialog (the row still holds the stale
268
282
  // updated_at captured at render time, so the DELETE 409s).
@@ -158,8 +158,7 @@ export default function IntegrationSetupWidget({ context }: InjectionWidgetCompo
158
158
  )}
159
159
  </p>
160
160
 
161
- <Alert>
162
- <Webhook className="h-4 w-4" />
161
+ <Alert icon={<Webhook aria-hidden="true" />}>
163
162
  <AlertTitle>
164
163
  {isEnabled
165
164
  ? t('webhooks.integrationSetup.enabledTitle', 'Delivery processing is enabled')