Skip to content

Commit fdc0a01

Browse files
Iris Alexandrescuclaude
andcommitted
feat(serenity): expose semrush provisioning status on brand reads (LLMO-7352/LLMO-7418)
Surfaces brands.semrush_provisioning_status/semrush_provisioning_error as read-only fields (semrushProvisioningStatus/semrushProvisioningError) on the V2 Brand DTO. These columns previously existed only for the async provisioning worker's own internal CAS bookkeeping (getBrandProvisioningState's narrow PROVISIONING_SELECT) -- no GET response exposed them, so a client had no way to show a persistent provisioning-status indicator without relying on an `async: true` create/activate response's one-shot jobId. mapDbBrandToV2 is the single mapping point for all 3 GET paths (listBrandsForOrg/getBrandForOrg/getBrandForOrgSite) and the create/update write-response paths (all route through withSerenityState, which just spreads whatever the mapper produced) -- fixing it there propagates everywhere for free, no SELECT/query changes needed since PostgREST's `'*'` already returns every brands column. Prerequisite for the frontend UI-states work (LLMO-7419): a persistent Setting-up/Ready/Setup-failed indicator on the brand list/detail views can't exist without a backend field to read it from. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent f77926a commit fdc0a01

3 files changed

Lines changed: 61 additions & 0 deletions

File tree

docs/openapi/schemas.yaml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7920,6 +7920,27 @@ V2Brand:
79207920
description: >-
79217921
Read-only deferred Semrush provisioning data for a pending (draft)
79227922
brand. Null for a non-pending brand. See V2PendingSemrushProvisioning.
7923+
semrushProvisioningStatus:
7924+
type:
7925+
- string
7926+
- 'null'
7927+
enum: [pending, ready, failed, null]
7928+
readOnly: true
7929+
description: >-
7930+
Read-only (LLMO-7352/LLMO-7418): the brand's async Semrush
7931+
provisioning state, so the caller can show a persistent Setting-up/
7932+
Ready/Setup-failed indicator without relying on an `async: true`
7933+
create/activate response's one-shot `jobId`. Null for a brand that
7934+
has never gone through an async provisioning attempt.
7935+
semrushProvisioningError:
7936+
type:
7937+
- string
7938+
- 'null'
7939+
readOnly: true
7940+
description: >-
7941+
Read-only (LLMO-7352/LLMO-7418): the failure detail recorded on the
7942+
last failed provisioning attempt. Null unless
7943+
`semrushProvisioningStatus` is `failed`.
79237944
updatedAt:
79247945
$ref: '#/DateTime'
79257946
updatedBy:

src/support/brands-storage.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,18 @@ function mapDbBrandToV2(row) {
312312
// sub-workspace minted yet). Consumers use it to scope per-brand Semrush
313313
// views to the sub-workspace.
314314
semrushSubWorkspaceId: row.semrush_sub_workspace_id || null,
315+
// Read-only (LLMO-7352/LLMO-7418): the brand's async Semrush provisioning state — the
316+
// same CAS state the provisioning worker itself reads/writes (see
317+
// beginProvisioningAttempt/promoteProvisioningReady/promoteProvisioningFailed below), now
318+
// surfaced to a normal GET so the frontend can show a persistent Setting-up/Ready/Setup-
319+
// failed indicator instead of relying on the one-shot `jobId` an `async: true` create/
320+
// activate response carries. Null for a brand that has never gone through an async
321+
// provisioning attempt (every brand created before this column existed, and every
322+
// synchronous create/activate).
323+
semrushProvisioningStatus: row.semrush_provisioning_status || null,
324+
// Read-only (LLMO-7352/LLMO-7418): the failure detail recorded on the LAST failed
325+
// provisioning attempt. Null unless semrushProvisioningStatus is 'failed'.
326+
semrushProvisioningError: row.semrush_provisioning_error || null,
315327
// Read-only: deferred Semrush provisioning data for a pending (draft) brand
316328
// (serenity dual-mode). Object { primaryUrl, markets: [{ market,
317329
// languageCode }] } the wizard collected before provisioning; null once

test/support/brands-storage.test.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,34 @@ describe('brands-storage', () => {
412412
expect(flatResult.pendingSemrushProvisioning).to.equal(null);
413413
});
414414

415+
it('maps semrush_provisioning_status/error to semrushProvisioningStatus/Error, null when absent (LLMO-7352/LLMO-7418)', async () => {
416+
const failedRow = makeBrandRow({
417+
semrush_provisioning_status: 'failed',
418+
semrush_provisioning_error: 'Semrush sub-workspace provisioning failed and cannot be recovered automatically.',
419+
});
420+
const failedQuery = createChainableQuery({ data: failedRow, error: null });
421+
const failedResult = await getBrandById(
422+
ORG_ID,
423+
BRAND_ID,
424+
{ from: sinon.stub().returns(failedQuery) },
425+
);
426+
expect(failedResult.semrushProvisioningStatus).to.equal('failed');
427+
expect(failedResult.semrushProvisioningError).to.equal(
428+
'Semrush sub-workspace provisioning failed and cannot be recovered automatically.',
429+
);
430+
431+
// A brand that has never gone through an async provisioning attempt (every brand
432+
// created before these columns existed, and every synchronous create/activate).
433+
const flatQuery = createChainableQuery({ data: makeBrandRow(), error: null });
434+
const flatResult = await getBrandById(
435+
ORG_ID,
436+
BRAND_ID,
437+
{ from: sinon.stub().returns(flatQuery) },
438+
);
439+
expect(flatResult.semrushProvisioningStatus).to.equal(null);
440+
expect(flatResult.semrushProvisioningError).to.equal(null);
441+
});
442+
415443
it('defaults to empty regions when competitor regions is missing', async () => {
416444
const dbRow = makeBrandRow({
417445
competitors: [{ name: 'Rival', url: null }], // no regions key — triggers || []

0 commit comments

Comments
 (0)