Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
5721f0d
marketcheck: failing tests for settings, the run and the suggestion t…
futurebuildai Oct 14, 2026
ce37c6e
marketcheck: settings, the suggestion rule and the run against the flow
futurebuildai Oct 14, 2026
50d0057
marketcheck: routes, Postgres adapters, wiring, contract, walls and w…
futurebuildai Oct 14, 2026
77d7afb
docs: the Market check flow contract, the contract change row and the…
futurebuildai Oct 14, 2026
3e1d0f8
docs: walk heading level
futurebuildai Oct 14, 2026
e2db368
desk: the Market check screen, its Tech Admin card and the Pricing link
futurebuildai Oct 14, 2026
8b8efdd
serve: gofmt the import order
futurebuildai Oct 14, 2026
eeb01da
key allowlist: the Market check run is refused to a branch bound key
futurebuildai Oct 14, 2026
9cda272
desk: the Market check run waits 90 seconds and reads an abort in words
futurebuildai Oct 14, 2026
e8e776c
desk: make the Market check service test catch the abort legs
futurebuildai Oct 14, 2026
6f7d371
desk: the Market check card keeps every field set before a re-render
futurebuildai Oct 14, 2026
e9c0a03
desk: the first pick is the yard's top Lumber sellers from the counte…
futurebuildai Oct 14, 2026
eb7e639
desk: the Market check fallback pick is fixed length dimensional lumber
futurebuildai Oct 14, 2026
7be45f6
desk: the Market check fallback pick is 2x4 to 2x12 dimensional lumbe…
futurebuildai Oct 14, 2026
9ad8908
marketcheck: failing tests for fix round 1 (deadline, stored settings…
futurebuildai Oct 14, 2026
3b3d725
marketcheck: write deadline for the run, stored settings re-validated…
futurebuildai Oct 14, 2026
fdf0af0
marketcheck: the settings notice, the card and page words, the Tech A…
futurebuildai Oct 14, 2026
60a3f31
Merge refactor/v1 into refactor/market-check
futurebuildai Oct 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions core/api/ROUTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ POST /api/v1/admin/exposure-scan internal/pricing h.HandleAdminScan
GET /api/v1/admin/keys internal/techadmin h.ListKeys
POST /api/v1/admin/keys internal/techadmin h.CreateKey
DELETE /api/v1/admin/keys/{id} internal/techadmin h.RevokeKey
GET /api/v1/admin/market-check/settings internal/marketcheck wrap(settingsGuard, h.getSettings)
PUT /api/v1/admin/market-check/settings internal/marketcheck wrap(settingsGuard, h.putSettings)
GET /api/v1/admin/modules internal/staff h.ListModules
PUT /api/v1/admin/modules/{id} internal/staff h.SetModuleEnabled
DELETE /api/v1/admin/settings/ai internal/techadmin h.DeleteAISettings
Expand Down Expand Up @@ -339,6 +341,7 @@ POST /api/v1/pricing/category-rules/bulk internal/pricing h.HandleBulkUpsertRule
DELETE /api/v1/pricing/category-rules/{id} internal/pricing h.HandleDeleteCategoryRule
PUT /api/v1/pricing/category-rules/{id} internal/pricing h.HandleUpdateCategoryRule
GET /api/v1/pricing/category-rules/{id}/audit internal/pricing h.HandleGetRuleAudit
POST /api/v1/pricing/market-check internal/marketcheck wrap(runGuard, h.run)
GET /api/v1/pricing/matrix internal/pricing h.HandleGetMatrix
GET /api/v1/pricing/rebates/programs internal/pricing h.HandleListPrograms
POST /api/v1/pricing/rebates/programs internal/pricing h.HandleCreateProgram
Expand Down
188 changes: 188 additions & 0 deletions core/api/fragments/marketcheck.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
# SPDX-License-Identifier: LicenseRef-OpenLBM-Commons-1.0
# SPDX-FileCopyrightText: 2026 FutureBuild, Inc. and OpenLBM contributors
#
# Market check (FROM-PRINCIPAL item 112 c): our walk in prices against a live
# competitor store, read through an FB Connect pricing flow by its webhook. The
# webhook id is a secret: it is sealed at rest and never answered. The flow's
# request and answer are documented in docs/integrations/MARKET-CHECK-FLOW.md.
# Component schemas are prefixed MarketCheck.
paths:
/api/v1/admin/market-check/settings:
get:
operationId: marketCheckGetSettings
tags: [pricing]
summary: Show the Market check settings
description: >-
Admin or owner. The FB Connect address, whether a webhook id is saved
(never the id), the flow's name, the comparison store's label, id and
ZIP, the band percent and the margin floor percent. configured is true
when the address and the id are both saved.
responses:
"200":
description: The settings.
content:
application/json:
schema:
$ref: "#/components/schemas/MarketCheckSettings"
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
"500": { $ref: "#/components/responses/WireInternalError" }
put:
operationId: marketCheckSaveSettings
tags: [pricing]
summary: Save the Market check settings
description: >-
Admin or owner. A field left out keeps what is saved. webhook_id absent
keeps the saved id, an empty string clears it; a new id is sealed with
the vault (refused in words when no vault key is configured). The
address is https://host[:port] with no user info, path, query or
fragment (http only for a loopback host). A request that changes the
address clears the saved id unless it carries a new one. Answers the
GET shape.
parameters:
- $ref: "#/components/parameters/IdempotencyKey"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/MarketCheckSettingsRequest"
responses:
"200":
description: The settings after the save.
content:
application/json:
schema:
$ref: "#/components/schemas/MarketCheckSettings"
"400": { $ref: "#/components/responses/WireBadRequest" }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
"413": { $ref: "#/components/responses/PayloadTooLarge" }
"500": { $ref: "#/components/responses/WireInternalError" }
/api/v1/pricing/market-check:
post:
operationId: marketCheckRun
tags: [pricing]
summary: Check picked products against the live store
description: >-
Admin or owner. Runs the FB Connect pricing flow for one to twelve
distinct products in batches of three. Per product: our walk in price,
the competitor's matched title and price, the difference and position
recomputed from our own figures, and a suggested price that keeps the
margin floor. A batch that fails marks only its own items error. 409
not_configured when the address or the webhook id is not saved or
usable. One run at a time per caller: a second answers 429. The
response gets its own write deadline past one batch. Cost is never in
the answer and is never sent to the flow.
parameters:
- $ref: "#/components/parameters/IdempotencyKey"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/MarketCheckRunRequest"
responses:
"200":
description: One row per picked product, in request order.
content:
application/json:
schema:
$ref: "#/components/schemas/MarketCheckResult"
"400": { $ref: "#/components/responses/WireBadRequest" }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
"409": { $ref: "#/components/responses/WireConflict" }
"413": { $ref: "#/components/responses/PayloadTooLarge" }
"429":
description: This caller already has a market check running; one at a time.
content:
application/json:
schema:
$ref: "#/components/schemas/WireError"
"500": { $ref: "#/components/responses/WireInternalError" }
components:
schemas:
MarketCheckSettings:
type: object
required: [connect_url, webhook_id_saved, flow_name, store_label, store_id, store_zip, band_pct, margin_floor_pct, configured, notice]
properties:
connect_url: {type: string}
webhook_id_saved: {type: boolean}
flow_name: {type: string}
store_label: {type: string}
store_id: {type: string}
store_zip: {type: string}
band_pct: {type: string, description: A decimal percent from 0 to 50.}
margin_floor_pct: {type: string, description: A decimal percent from 0 to 90.}
configured:
type: boolean
description: True only when the address and the id are saved and usable (the stored id opens and the address is valid).
notice:
type: string
description: A plain sentence when the saved state needs the admin (an id to enter again); empty otherwise.
MarketCheckSettingsRequest:
type: object
additionalProperties: false
properties:
connect_url: {type: string}
webhook_id: {type: string, description: Write only. Absent keeps the saved id; an empty string clears it.}
flow_name: {type: string}
store_label: {type: string}
store_id: {type: string}
store_zip: {type: string}
band_pct: {type: string}
margin_floor_pct: {type: string}
MarketCheckRunRequest:
type: object
additionalProperties: false
required: [product_ids]
properties:
product_ids:
type: array
minItems: 1
maxItems: 12
items: {type: string, format: uuid}
MarketCheckCompetitor:
type: object
required: [title, price_cents, listed_price_cents, link, confidence, unit_note]
properties:
title: {type: string}
price_cents: {type: integer, format: int64}
listed_price_cents: {type: integer, format: int64}
link: {type: string, description: Kept only when it starts with https://www.homedepot.com/, else empty.}
confidence: {type: integer, minimum: 0, maximum: 100}
unit_note: {type: string}
MarketCheckRow:
type: object
required: [product_id, sku, description, uom, our_price_cents, cost_known, status, competitor, diff_pct, position, suggested_price_cents, reason, note]
properties:
product_id: {type: string, format: uuid}
sku: {type: string}
description: {type: string}
uom: {type: string}
our_price_cents: {type: integer, format: int64}
cost_known: {type: boolean}
status: {type: string, enum: [matched, unmatched, error]}
competitor:
nullable: true
allOf:
- $ref: "#/components/schemas/MarketCheckCompetitor"
diff_pct: {type: number, nullable: true}
position: {type: string, enum: ["", above, below, in_line]}
suggested_price_cents: {type: integer, format: int64, nullable: true}
reason: {type: string, enum: ["", lower_to_band, raise_to_band, held_at_floor, keep, no_cost]}
note: {type: string}
MarketCheckResult:
type: object
required: [flow_name, store_label, store_seen, band_pct, margin_floor_pct, rows]
properties:
flow_name: {type: string}
store_label: {type: string}
store_seen: {type: string, nullable: true}
band_pct: {type: string}
margin_floor_pct: {type: string}
rows:
type: array
items:
$ref: "#/components/schemas/MarketCheckRow"
160 changes: 160 additions & 0 deletions core/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8212,6 +8212,83 @@ paths:
$ref: "#/components/responses/UnprocessableEntity"
"500":
$ref: "#/components/responses/InternalError"
/api/v1/admin/market-check/settings:
get:
operationId: marketCheckGetSettings
tags: [pricing]
summary: Show the Market check settings
description: >-
Admin or owner. The FB Connect address, whether a webhook id is saved (never the id), the flow's name, the comparison store's label, id and ZIP, the band percent and the margin floor percent. configured is true when the address and the id are both saved.
responses:
"200":
description: The settings.
content:
application/json:
schema:
$ref: "#/components/schemas/MarketCheckSettings"
"401": {$ref: "#/components/responses/Unauthorized"}
"403": {$ref: "#/components/responses/Forbidden"}
"500": {$ref: "#/components/responses/WireInternalError"}
put:
operationId: marketCheckSaveSettings
tags: [pricing]
summary: Save the Market check settings
description: >-
Admin or owner. A field left out keeps what is saved. webhook_id absent keeps the saved id, an empty string clears it; a new id is sealed with the vault (refused in words when no vault key is configured). The address is https://host[:port] with no user info, path, query or fragment (http only for a loopback host). A request that changes the address clears the saved id unless it carries a new one. Answers the GET shape.
parameters:
- $ref: "#/components/parameters/IdempotencyKey"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/MarketCheckSettingsRequest"
responses:
"200":
description: The settings after the save.
content:
application/json:
schema:
$ref: "#/components/schemas/MarketCheckSettings"
"400": {$ref: "#/components/responses/WireBadRequest"}
"401": {$ref: "#/components/responses/Unauthorized"}
"403": {$ref: "#/components/responses/Forbidden"}
"413": {$ref: "#/components/responses/PayloadTooLarge"}
"500": {$ref: "#/components/responses/WireInternalError"}
/api/v1/pricing/market-check:
post:
operationId: marketCheckRun
tags: [pricing]
summary: Check picked products against the live store
description: >-
Admin or owner. Runs the FB Connect pricing flow for one to twelve distinct products in batches of three. Per product: our walk in price, the competitor's matched title and price, the difference and position recomputed from our own figures, and a suggested price that keeps the margin floor. A batch that fails marks only its own items error. 409 not_configured when the address or the webhook id is not saved or usable. One run at a time per caller: a second answers 429. The response gets its own write deadline past one batch. Cost is never in the answer and is never sent to the flow.
parameters:
- $ref: "#/components/parameters/IdempotencyKey"
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/MarketCheckRunRequest"
responses:
"200":
description: One row per picked product, in request order.
content:
application/json:
schema:
$ref: "#/components/schemas/MarketCheckResult"
"400": {$ref: "#/components/responses/WireBadRequest"}
"401": {$ref: "#/components/responses/Unauthorized"}
"403": {$ref: "#/components/responses/Forbidden"}
"409": {$ref: "#/components/responses/WireConflict"}
"413": {$ref: "#/components/responses/PayloadTooLarge"}
"429":
description: This caller already has a market check running; one at a time.
content:
application/json:
schema:
$ref: "#/components/schemas/WireError"
"500": {$ref: "#/components/responses/WireInternalError"}
/api/v1/matching/config:
get:
operationId: matchingConfigGet
Expand Down Expand Up @@ -21966,6 +22043,89 @@ components:
type: array
items:
type: string
MarketCheckSettings:
type: object
required: [connect_url, webhook_id_saved, flow_name, store_label, store_id, store_zip, band_pct, margin_floor_pct, configured, notice]
properties:
connect_url: {type: string}
webhook_id_saved: {type: boolean}
flow_name: {type: string}
store_label: {type: string}
store_id: {type: string}
store_zip: {type: string}
band_pct: {type: string, description: A decimal percent from 0 to 50.}
margin_floor_pct: {type: string, description: A decimal percent from 0 to 90.}
configured:
type: boolean
description: True only when the address and the id are saved and usable (the stored id opens and the address is valid).
notice:
type: string
description: A plain sentence when the saved state needs the admin (an id to enter again); empty otherwise.
MarketCheckSettingsRequest:
type: object
additionalProperties: false
properties:
connect_url: {type: string}
webhook_id: {type: string, description: Write only. Absent keeps the saved id; an empty string clears it.}
flow_name: {type: string}
store_label: {type: string}
store_id: {type: string}
store_zip: {type: string}
band_pct: {type: string}
margin_floor_pct: {type: string}
MarketCheckRunRequest:
type: object
additionalProperties: false
required: [product_ids]
properties:
product_ids:
type: array
minItems: 1
maxItems: 12
items: {type: string, format: uuid}
MarketCheckCompetitor:
type: object
required: [title, price_cents, listed_price_cents, link, confidence, unit_note]
properties:
title: {type: string}
price_cents: {type: integer, format: int64}
listed_price_cents: {type: integer, format: int64}
link: {type: string, description: 'Kept only when it starts with https://www.homedepot.com/', else empty.: ''}
confidence: {type: integer, minimum: 0, maximum: 100}
unit_note: {type: string}
MarketCheckRow:
type: object
required: [product_id, sku, description, uom, our_price_cents, cost_known, status, competitor, diff_pct, position, suggested_price_cents, reason, note]
properties:
product_id: {type: string, format: uuid}
sku: {type: string}
description: {type: string}
uom: {type: string}
our_price_cents: {type: integer, format: int64}
cost_known: {type: boolean}
status: {type: string, enum: [matched, unmatched, error]}
competitor:
nullable: true
allOf:
- $ref: "#/components/schemas/MarketCheckCompetitor"
diff_pct: {type: number, nullable: true}
position: {type: string, enum: ["", above, below, in_line]}
suggested_price_cents: {type: integer, format: int64, nullable: true}
reason: {type: string, enum: ["", lower_to_band, raise_to_band, held_at_floor, keep, no_cost]}
note: {type: string}
MarketCheckResult:
type: object
required: [flow_name, store_label, store_seen, band_pct, margin_floor_pct, rows]
properties:
flow_name: {type: string}
store_label: {type: string}
store_seen: {type: string, nullable: true}
band_pct: {type: string}
margin_floor_pct: {type: string}
rows:
type: array
items:
$ref: "#/components/schemas/MarketCheckRow"
MatchingStatus:
type: string
enum: [PENDING, MATCHED, PARTIAL, EXCEPTION]
Expand Down
Loading
Loading