feat(research): sync company product update client (#31)

main v0.7.23
Vernon Keenan 2026-07-27 00:18:17 -07:00 committed by GitHub
parent abc0ef7918
commit 2da20d2d25
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 1021 additions and 51 deletions

View File

@ -51,7 +51,7 @@ pins the same module, a `lib` bump is a constellation-wide rebuild (KV-C6a).
| `app/service-helpers.go` | Pointer helpers (`String`/`Bool`/`Int64`/…), `GetFieldsAndValues` reflection helper (unit-tested) |
| `app/user.go`, `user-helpers.go`, `tenantuser.go`, `userrole.go`, `address.go` | Compatibility principal/domain structs. ADR-KV-024 adds `PrincipalKey`, `PrincipalType`, exact `Scopes`, `HasScope`, and `IsServicePrincipal`; no native response carries its bearer secret. |
| `app/logger/logger.go` | Thin `zap.SugaredLogger` wrapper (`New(level)`, level constants) |
| `api/{auth,crm,members,plex,research,sfgate,stash}/` | go-swaggergenerated typed clients + models, one directory per sibling service. Stash is intentionally generated from the read-only PDF metadata projection described below. Research CompanyProduct/CompanyService operations are pinned to provider PR #32 as documented in `docs/RESEARCH_COMPANY_CATALOG_CLIENT.md`. |
| `api/{auth,crm,members,plex,research,sfgate,stash}/` | go-swaggergenerated typed clients + models, one directory per sibling service. Stash is intentionally generated from the read-only PDF metadata projection described below. Research CompanyProduct/CompanyService operations are pinned to PR #57 source `c3127f32872051b842c74ae7139b1c6ef71e2c53`, merge `cc7bf6191dc105969c804e123823186f1ea26cc3`, and spec SHA-256 `1e5b34b5c7b4230cfd507995ab1f837aabc7b4dcdf61b114f37012167afdf224` as documented in `docs/RESEARCH_COMPANY_CATALOG_CLIENT.md`. |
| `swagger/*.yaml`, `swagger/defs/`, `swagger/external/` | Copies of each sibling service's spec (the KV-C6b spec-copy problem — no sync mechanism besides `make regen`), shared component defs, and host/path-rewritten copies for external doc publishing; also carries legacy vendored Kazoo/Clerk specs from `lib`'s pre-mesh history |
## Build, run, test
@ -89,13 +89,16 @@ fields; it cannot generate the provider's deprecated, fail-closed create
compatibility route. See `docs/STASH_PDF_METADATA_CLIENT.md` for the provider
commit and release blockers.
The Research specification is synchronized from provider PR #32 at commit
`ca6a2911ce79d053f9aa338e6d9bcfe3357b0c77`. Its generated company catalog
surface exposes Product and Service list/detail/create plus Service CAS update.
Product direct update remains proposal-only, and both catalog deletes are
absent. See `docs/RESEARCH_COMPANY_CATALOG_CLIENT.md` for the pinned spec hash,
exact scope requirements, database assumptions, and Research-before-Lib-before-
Studio release gate.
The Research specification is synchronized from PR #57 source
`c3127f32872051b842c74ae7139b1c6ef71e2c53`, merged as
`cc7bf6191dc105969c804e123823186f1ea26cc3`. Its generated company catalog
surface exposes Product and Service list/detail/create plus optimistic-
concurrency Product and Service updates. Published Product changes remain
proposal-only, legacy proposal candidates are never silently converted to
direct writes, and both catalog deletes are absent. See
`docs/RESEARCH_COMPANY_CATALOG_CLIENT.md` for the pinned spec hash, exact scope
requirements, database assumptions, and Research-before-Lib-before-Studio
release gate.
**Known gap (resolved for the current spec set, but can recur):** if any
sibling spec has an operation with no top-level `tags:`, go-swagger routes

View File

@ -36,6 +36,8 @@ func (transport *catalogCaptureTransport) SubmitContext(
return company_products.NewGetCompanyProductsOK(), nil
case "postCompanyProducts":
return company_products.NewPostCompanyProductsOK(), nil
case "putCompanyProduct":
return company_products.NewPutCompanyProductOK(), nil
case "getCompanyServices":
return company_services.NewGetCompanyServicesOK(), nil
case "postCompanyServices":
@ -74,6 +76,15 @@ func TestGeneratedCompanyCatalogOperationContract(t *testing.T) {
return err
},
},
{
name: "CAS update product", wantID: "putCompanyProduct",
wantMethod: "PUT", wantPath: "/companyproducts",
call: func(transport openapiruntime.ContextualTransport) error {
_, err := company_products.New(transport, strfmt.Default).
PutCompanyProduct(company_products.NewPutCompanyProductParams(), nil)
return err
},
},
{
name: "list or get service", wantID: "getCompanyServices",
wantMethod: "GET", wantPath: "/companyservices",
@ -143,6 +154,10 @@ func TestGeneratedCompanyCatalogListGetAndMutationShapes(t *testing.T) {
WithCompanyProductRequest(productRequest).CompanyProductRequest; got != productRequest {
t.Fatal("CompanyProduct create request body is absent")
}
if got := company_products.NewPutCompanyProductParams().
WithCompanyProductRequest(productRequest).CompanyProductRequest; got != productRequest {
t.Fatal("CompanyProduct CAS update request body is absent")
}
serviceRequest := &research_models.CompanyServiceRequest{
Data: []*research_models.CompanyService{{
@ -165,6 +180,11 @@ func TestGeneratedCompanyCatalogNegativeMutationResponses(t *testing.T) {
"product forbidden": company_products.NewPostCompanyProductsForbidden(),
"product not found": company_products.NewPostCompanyProductsNotFound(),
"product invalid": company_products.NewPostCompanyProductsUnprocessableEntity(),
"product update unauthorized": company_products.NewPutCompanyProductUnauthorized(),
"product update forbidden": company_products.NewPutCompanyProductForbidden(),
"product update not found": company_products.NewPutCompanyProductNotFound(),
"product update conflict": company_products.NewPutCompanyProductConflict(),
"product update invalid": company_products.NewPutCompanyProductUnprocessableEntity(),
"service unauthorized": company_services.NewPostCompanyServicesUnauthorized(),
"service forbidden": company_services.NewPostCompanyServicesForbidden(),
"service invalid": company_services.NewPostCompanyServicesUnprocessableEntity(),
@ -178,6 +198,9 @@ func TestGeneratedCompanyCatalogNegativeMutationResponses(t *testing.T) {
expected := map[string]int{
"product unauthorized": 401, "product forbidden": 403,
"product not found": 404, "product invalid": 422,
"product update unauthorized": 401, "product update forbidden": 403,
"product update not found": 404, "product update conflict": 409,
"product update invalid": 422,
"service unauthorized": 401, "service forbidden": 403,
"service invalid": 422, "update unauthorized": 401,
"update forbidden": 403, "update not found": 404,
@ -193,7 +216,6 @@ func TestGeneratedCompanyCatalogNegativeMutationResponses(t *testing.T) {
func TestGeneratedCompanyCatalogDoesNotExposeUnsafeMutations(t *testing.T) {
productContract := reflect.TypeOf((*company_products.ClientService)(nil)).Elem()
for _, method := range []string{
"PutCompanyProduct", "PutCompanyProductContext",
"DeleteCompanyProduct", "DeleteCompanyProductContext",
} {
if _, exists := productContract.MethodByName(method); exists {
@ -217,11 +239,18 @@ func TestCompanyCatalogProviderProjectionRequiresCompoundAuth(t *testing.T) {
}
products := document.Spec().Paths.Paths["/companyproducts"]
services := document.Spec().Paths.Paths["/companyservices"]
if products.Put == nil {
t.Fatal("provider projection is missing CompanyProduct CAS update")
}
if services.Put == nil {
t.Fatal("provider projection is missing CompanyService CAS update")
}
operations := []*struct {
Security []map[string][]string
}{
{Security: products.Get.Security},
{Security: products.Post.Security},
{Security: products.Put.Security},
{Security: services.Get.Security},
{Security: services.Post.Security},
{Security: services.Put.Security},
@ -237,8 +266,21 @@ func TestCompanyCatalogProviderProjectionRequiresCompoundAuth(t *testing.T) {
t.Fatal("catalog operation is missing kvSessionCookie")
}
}
if products.Put != nil || products.Delete != nil || services.Delete != nil {
t.Fatal("provider projection reintroduced Product PUT or a catalog DELETE")
for _, fragment := range []string{
"optimistic concurrency",
"preserving its immutable Account binding",
"publication state",
"enrichment audit",
"media ownership",
"Published Products require the governed proposal workflow",
"research:company-product:update",
} {
if !strings.Contains(products.Put.Description, fragment) {
t.Errorf("CompanyProduct PUT description is missing %q", fragment)
}
}
if products.Delete != nil || services.Delete != nil {
t.Fatal("provider projection exposes a catalog DELETE")
}
}

View File

@ -78,6 +78,12 @@ type ClientService interface {
// PostCompanyProductsContext create one tenant owned company product.
PostCompanyProductsContext(ctx context.Context, params *PostCompanyProductsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*PostCompanyProductsOK, error)
// PutCompanyProduct update one unpublished shared catalog company product.
PutCompanyProduct(params *PutCompanyProductParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*PutCompanyProductOK, error)
// PutCompanyProductContext update one unpublished shared catalog company product.
PutCompanyProductContext(ctx context.Context, params *PutCompanyProductParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*PutCompanyProductOK, error)
SetTransport(transport runtime.ContextualTransport)
}
@ -217,7 +223,7 @@ func (a *Client) GetPublishedCompanyProductsContext(ctx context.Context, params
// PostCompanyProducts creates one tenant owned company product.
//
// Create one unpublished CompanyProduct beneath an Account owned by the machine tenant; requires research:company-product:create and same-tenant Owner or Manager. Existing Product updates remain proposal-only..
// Create one unpublished CompanyProduct beneath an Account owned by the machine tenant; requires research:company-product:create and same-tenant Owner or Manager.
//
// This method does not support injected context.
// However, timeout and opentracing contexts are honored whenever enabled.
@ -236,7 +242,7 @@ func (a *Client) PostCompanyProducts(params *PostCompanyProductsParams, authInfo
// PostCompanyProductsContext creates one tenant owned company product.
//
// Create one unpublished CompanyProduct beneath an Account owned by the machine tenant; requires research:company-product:create and same-tenant Owner or Manager. Existing Product updates remain proposal-only..
// Create one unpublished CompanyProduct beneath an Account owned by the machine tenant; requires research:company-product:create and same-tenant Owner or Manager.
//
// Do not use the deprecated [PostCompanyProductsParams.Context] with this method: it would be ignored.
func (a *Client) PostCompanyProductsContext(ctx context.Context, params *PostCompanyProductsParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*PostCompanyProductsOK, error) {
@ -282,6 +288,73 @@ func (a *Client) PostCompanyProductsContext(ctx context.Context, params *PostCom
panic(msg)
}
// PutCompanyProduct updates one unpublished shared catalog company product.
//
// Replace the editable fields of one shared-catalog CompanyProduct using optimistic concurrency while preserving its immutable Account binding, publication state, enrichment audit, and media ownership. Published Products require the governed proposal workflow. Requires research:company-product:update and a same-tenant Owner or Manager..
//
// This method does not support injected context.
// However, timeout and opentracing contexts are honored whenever enabled.
//
// If you need to pass a specific context, use [Client.PutCompanyProductContext] instead.
func (a *Client) PutCompanyProduct(params *PutCompanyProductParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*PutCompanyProductOK, error) {
var ctx context.Context
if params.inner.ctx != nil {
ctx = params.inner.ctx
} else {
ctx = context.Background()
}
return a.PutCompanyProductContext(ctx, params, authInfo, opts...)
}
// PutCompanyProductContext updates one unpublished shared catalog company product.
//
// Replace the editable fields of one shared-catalog CompanyProduct using optimistic concurrency while preserving its immutable Account binding, publication state, enrichment audit, and media ownership. Published Products require the governed proposal workflow. Requires research:company-product:update and a same-tenant Owner or Manager..
//
// Do not use the deprecated [PutCompanyProductParams.Context] with this method: it would be ignored.
func (a *Client) PutCompanyProductContext(ctx context.Context, params *PutCompanyProductParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*PutCompanyProductOK, error) {
// NOTE: parameters are not validated before sending
if params == nil {
params = NewPutCompanyProductParams()
}
op := &runtime.ClientOperation{
ID: "putCompanyProduct",
Method: "PUT",
PathPattern: "/companyproducts",
ProducesMediaTypes: []string{"application/json"},
ConsumesMediaTypes: []string{"application/json"},
Schemes: []string{"http"},
Params: params,
Reader: &PutCompanyProductReader{formats: a.formats},
AuthInfo: authInfo,
Client: params.HTTPClient,
}
for _, opt := range opts {
opt(op)
}
result, err := a.transport.SubmitContext(ctx, op)
if err != nil {
return nil, err
}
// only one success response has to be checked
success, ok := result.(*PutCompanyProductOK)
if ok {
return success, nil
}
// unexpected success response.
// no default response is defined.
//
// safeguard: normally, in the absence of a default response, unknown success responses return an error above: so this is a codegen issue
msg := fmt.Sprintf("unexpected success response for putCompanyProduct: API contract not enforced by server. Client expected to get an error, but got: %T", result)
panic(msg)
}
// SetTransport changes the transport on the client
func (a *Client) SetTransport(transport runtime.ContextualTransport) {
a.transport = transport

View File

@ -0,0 +1,159 @@
// Code generated by go-swagger; DO NOT EDIT.
// (c) 2012-2020 by Taxnexus, Inc.
// All rights reserved worldwide.
// Proprietary product; unlicensed use is not allowed
package company_products
import (
"context"
"net/http"
"time"
"code.tnxs.net/vernonkeenan/lib/api/research/research_models"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime"
cr "github.com/go-openapi/runtime/client"
"github.com/go-openapi/strfmt"
)
// NewPutCompanyProductParams creates a new PutCompanyProductParams object,
// with the default timeout for this client.
//
// Default values are not hydrated, since defaults are normally applied by the API server side.
//
// To enforce default values in parameter, use SetDefaults or WithDefaults.
func NewPutCompanyProductParams() *PutCompanyProductParams {
return NewPutCompanyProductParamsWithTimeout(cr.DefaultTimeout)
}
// NewPutCompanyProductParamsWithTimeout creates a new PutCompanyProductParams object
// with the ability to set a timeout on a request.
func NewPutCompanyProductParamsWithTimeout(timeout time.Duration) *PutCompanyProductParams {
return &PutCompanyProductParams{
inner: innerParams{
timeout: timeout,
},
}
}
// NewPutCompanyProductParamsWithContext creates a new PutCompanyProductParams object
// with the ability to set a context for a request.
//
// Deprecated: use the operation call with context to pass the context instead of [PutCompanyProductParams].
func NewPutCompanyProductParamsWithContext(ctx context.Context) *PutCompanyProductParams {
return &PutCompanyProductParams{
inner: innerParams{
ctx: ctx,
},
}
}
// NewPutCompanyProductParamsWithHTTPClient creates a new PutCompanyProductParams object
// with the ability to set a custom HTTPClient for a request.
func NewPutCompanyProductParamsWithHTTPClient(client *http.Client) *PutCompanyProductParams {
return &PutCompanyProductParams{
HTTPClient: client,
}
}
/*
PutCompanyProductParams contains all the parameters to send to the API endpoint
for the put company product operation.
Typically these are written to a http.Request.
*/
type PutCompanyProductParams struct {
// CompanyProductRequest.
//
// An array of new CompanyProduct records
CompanyProductRequest *research_models.CompanyProductRequest
HTTPClient *http.Client
inner innerParams
}
// WithDefaults hydrates default values in the put company product params (not the query body).
//
// All values with no default are reset to their zero value.
func (o *PutCompanyProductParams) WithDefaults() *PutCompanyProductParams {
o.SetDefaults()
return o
}
// SetDefaults hydrates default values in the put company product params (not the query body).
//
// All values with no default are reset to their zero value.
func (o *PutCompanyProductParams) SetDefaults() {
// no default values defined for this parameter
}
// WithTimeout adds the timeout to the put company product params.
func (o *PutCompanyProductParams) WithTimeout(timeout time.Duration) *PutCompanyProductParams {
o.SetTimeout(timeout)
return o
}
// SetTimeout adds the timeout to the put company product params.
func (o *PutCompanyProductParams) SetTimeout(timeout time.Duration) {
o.inner.timeout = timeout
}
// WithContext adds the context to the put company product params.
//
// Deprecated: use the operation call with context to pass the context instead of [PutCompanyProductParams].
func (o *PutCompanyProductParams) WithContext(ctx context.Context) *PutCompanyProductParams {
o.SetContext(ctx)
return o
}
// SetContext adds the context to the put company product params.
//
// Deprecated: use the operation call with context to pass the context instead of [PutCompanyProductParams].
func (o *PutCompanyProductParams) SetContext(ctx context.Context) {
o.inner.ctx = ctx
}
// WithHTTPClient adds the HTTPClient to the put company product params.
func (o *PutCompanyProductParams) WithHTTPClient(client *http.Client) *PutCompanyProductParams {
o.SetHTTPClient(client)
return o
}
// SetHTTPClient adds the HTTPClient to the put company product params.
func (o *PutCompanyProductParams) SetHTTPClient(client *http.Client) {
o.HTTPClient = client
}
// WithCompanyProductRequest adds the companyProductRequest to the put company product params.
func (o *PutCompanyProductParams) WithCompanyProductRequest(companyProductRequest *research_models.CompanyProductRequest) *PutCompanyProductParams {
o.SetCompanyProductRequest(companyProductRequest)
return o
}
// SetCompanyProductRequest adds the companyProductRequest to the put company product params.
func (o *PutCompanyProductParams) SetCompanyProductRequest(companyProductRequest *research_models.CompanyProductRequest) {
o.CompanyProductRequest = companyProductRequest
}
// WriteToRequest writes these params to a [runtime.ClientRequest].
func (o *PutCompanyProductParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error {
if err := r.SetTimeout(o.inner.timeout); err != nil {
return err
}
var res []error
if o.CompanyProductRequest != nil {
if err := r.SetBodyParam(o.CompanyProductRequest); err != nil {
return err
}
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}

View File

@ -0,0 +1,603 @@
// Code generated by go-swagger; DO NOT EDIT.
// (c) 2012-2020 by Taxnexus, Inc.
// All rights reserved worldwide.
// Proprietary product; unlicensed use is not allowed
package company_products
import (
"encoding/json"
stderrors "errors"
"fmt"
"io"
"code.tnxs.net/vernonkeenan/lib/api/research/research_models"
"github.com/go-openapi/runtime"
"github.com/go-openapi/strfmt"
)
// PutCompanyProductReader is a Reader for the PutCompanyProduct structure.
type PutCompanyProductReader struct {
formats strfmt.Registry
}
// ReadResponse reads a server response into the received o.
func (o *PutCompanyProductReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (any, error) {
switch response.Code() {
case 200:
result := NewPutCompanyProductOK()
if err := result.readResponse(response, consumer, o.formats); err != nil {
return nil, err
}
return result, nil
case 401:
result := NewPutCompanyProductUnauthorized()
if err := result.readResponse(response, consumer, o.formats); err != nil {
return nil, err
}
return nil, result
case 403:
result := NewPutCompanyProductForbidden()
if err := result.readResponse(response, consumer, o.formats); err != nil {
return nil, err
}
return nil, result
case 404:
result := NewPutCompanyProductNotFound()
if err := result.readResponse(response, consumer, o.formats); err != nil {
return nil, err
}
return nil, result
case 409:
result := NewPutCompanyProductConflict()
if err := result.readResponse(response, consumer, o.formats); err != nil {
return nil, err
}
return nil, result
case 422:
result := NewPutCompanyProductUnprocessableEntity()
if err := result.readResponse(response, consumer, o.formats); err != nil {
return nil, err
}
return nil, result
case 500:
result := NewPutCompanyProductInternalServerError()
if err := result.readResponse(response, consumer, o.formats); err != nil {
return nil, err
}
return nil, result
default:
return nil, runtime.NewAPIError("[PUT /companyproducts] putCompanyProduct", response, response.Code())
}
}
// NewPutCompanyProductOK creates a PutCompanyProductOK with default headers values
func NewPutCompanyProductOK() *PutCompanyProductOK {
return &PutCompanyProductOK{}
}
// PutCompanyProductOK describes a response with status code 200, with default header values.
//
// Response with CompanyProduct objects
type PutCompanyProductOK struct {
Payload *research_models.CompanyProductResponse
}
// IsSuccess returns true when this put company product o k response has a 2xx status code
func (o *PutCompanyProductOK) IsSuccess() bool {
return true
}
// IsRedirect returns true when this put company product o k response has a 3xx status code
func (o *PutCompanyProductOK) IsRedirect() bool {
return false
}
// IsClientError returns true when this put company product o k response has a 4xx status code
func (o *PutCompanyProductOK) IsClientError() bool {
return false
}
// IsServerError returns true when this put company product o k response has a 5xx status code
func (o *PutCompanyProductOK) IsServerError() bool {
return false
}
// IsCode returns true when this put company product o k response a status code equal to that given
func (o *PutCompanyProductOK) IsCode(code int) bool {
return code == 200
}
// Code gets the status code for the put company product o k response
func (o *PutCompanyProductOK) Code() int {
return 200
}
func (o *PutCompanyProductOK) Error() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[PUT /companyproducts][%d] putCompanyProductOK %s", 200, payload)
}
func (o *PutCompanyProductOK) String() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[PUT /companyproducts][%d] putCompanyProductOK %s", 200, payload)
}
func (o *PutCompanyProductOK) GetPayload() *research_models.CompanyProductResponse {
return o.Payload
}
func (o *PutCompanyProductOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
o.Payload = new(research_models.CompanyProductResponse)
// response payload
if err := consumer.Consume(response.Body(), o.Payload); err != nil && !stderrors.Is(err, io.EOF) {
return err
}
return nil
}
// NewPutCompanyProductUnauthorized creates a PutCompanyProductUnauthorized with default headers values
func NewPutCompanyProductUnauthorized() *PutCompanyProductUnauthorized {
return &PutCompanyProductUnauthorized{}
}
// PutCompanyProductUnauthorized describes a response with status code 401, with default header values.
//
// Access unauthorized, invalid API-KEY was used
type PutCompanyProductUnauthorized struct {
AccessControlAllowOrigin string
Payload *research_models.Error
}
// IsSuccess returns true when this put company product unauthorized response has a 2xx status code
func (o *PutCompanyProductUnauthorized) IsSuccess() bool {
return false
}
// IsRedirect returns true when this put company product unauthorized response has a 3xx status code
func (o *PutCompanyProductUnauthorized) IsRedirect() bool {
return false
}
// IsClientError returns true when this put company product unauthorized response has a 4xx status code
func (o *PutCompanyProductUnauthorized) IsClientError() bool {
return true
}
// IsServerError returns true when this put company product unauthorized response has a 5xx status code
func (o *PutCompanyProductUnauthorized) IsServerError() bool {
return false
}
// IsCode returns true when this put company product unauthorized response a status code equal to that given
func (o *PutCompanyProductUnauthorized) IsCode(code int) bool {
return code == 401
}
// Code gets the status code for the put company product unauthorized response
func (o *PutCompanyProductUnauthorized) Code() int {
return 401
}
func (o *PutCompanyProductUnauthorized) Error() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[PUT /companyproducts][%d] putCompanyProductUnauthorized %s", 401, payload)
}
func (o *PutCompanyProductUnauthorized) String() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[PUT /companyproducts][%d] putCompanyProductUnauthorized %s", 401, payload)
}
func (o *PutCompanyProductUnauthorized) GetPayload() *research_models.Error {
return o.Payload
}
func (o *PutCompanyProductUnauthorized) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
// hydrates response header Access-Control-Allow-Origin
hdrAccessControlAllowOrigin := response.GetHeader("Access-Control-Allow-Origin")
if hdrAccessControlAllowOrigin != "" {
o.AccessControlAllowOrigin = hdrAccessControlAllowOrigin
}
o.Payload = new(research_models.Error)
// response payload
if err := consumer.Consume(response.Body(), o.Payload); err != nil && !stderrors.Is(err, io.EOF) {
return err
}
return nil
}
// NewPutCompanyProductForbidden creates a PutCompanyProductForbidden with default headers values
func NewPutCompanyProductForbidden() *PutCompanyProductForbidden {
return &PutCompanyProductForbidden{}
}
// PutCompanyProductForbidden describes a response with status code 403, with default header values.
//
// Access forbidden, account lacks access
type PutCompanyProductForbidden struct {
AccessControlAllowOrigin string
Payload *research_models.Error
}
// IsSuccess returns true when this put company product forbidden response has a 2xx status code
func (o *PutCompanyProductForbidden) IsSuccess() bool {
return false
}
// IsRedirect returns true when this put company product forbidden response has a 3xx status code
func (o *PutCompanyProductForbidden) IsRedirect() bool {
return false
}
// IsClientError returns true when this put company product forbidden response has a 4xx status code
func (o *PutCompanyProductForbidden) IsClientError() bool {
return true
}
// IsServerError returns true when this put company product forbidden response has a 5xx status code
func (o *PutCompanyProductForbidden) IsServerError() bool {
return false
}
// IsCode returns true when this put company product forbidden response a status code equal to that given
func (o *PutCompanyProductForbidden) IsCode(code int) bool {
return code == 403
}
// Code gets the status code for the put company product forbidden response
func (o *PutCompanyProductForbidden) Code() int {
return 403
}
func (o *PutCompanyProductForbidden) Error() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[PUT /companyproducts][%d] putCompanyProductForbidden %s", 403, payload)
}
func (o *PutCompanyProductForbidden) String() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[PUT /companyproducts][%d] putCompanyProductForbidden %s", 403, payload)
}
func (o *PutCompanyProductForbidden) GetPayload() *research_models.Error {
return o.Payload
}
func (o *PutCompanyProductForbidden) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
// hydrates response header Access-Control-Allow-Origin
hdrAccessControlAllowOrigin := response.GetHeader("Access-Control-Allow-Origin")
if hdrAccessControlAllowOrigin != "" {
o.AccessControlAllowOrigin = hdrAccessControlAllowOrigin
}
o.Payload = new(research_models.Error)
// response payload
if err := consumer.Consume(response.Body(), o.Payload); err != nil && !stderrors.Is(err, io.EOF) {
return err
}
return nil
}
// NewPutCompanyProductNotFound creates a PutCompanyProductNotFound with default headers values
func NewPutCompanyProductNotFound() *PutCompanyProductNotFound {
return &PutCompanyProductNotFound{}
}
// PutCompanyProductNotFound describes a response with status code 404, with default header values.
//
// Resource was not found
type PutCompanyProductNotFound struct {
AccessControlAllowOrigin string
Payload *research_models.Error
}
// IsSuccess returns true when this put company product not found response has a 2xx status code
func (o *PutCompanyProductNotFound) IsSuccess() bool {
return false
}
// IsRedirect returns true when this put company product not found response has a 3xx status code
func (o *PutCompanyProductNotFound) IsRedirect() bool {
return false
}
// IsClientError returns true when this put company product not found response has a 4xx status code
func (o *PutCompanyProductNotFound) IsClientError() bool {
return true
}
// IsServerError returns true when this put company product not found response has a 5xx status code
func (o *PutCompanyProductNotFound) IsServerError() bool {
return false
}
// IsCode returns true when this put company product not found response a status code equal to that given
func (o *PutCompanyProductNotFound) IsCode(code int) bool {
return code == 404
}
// Code gets the status code for the put company product not found response
func (o *PutCompanyProductNotFound) Code() int {
return 404
}
func (o *PutCompanyProductNotFound) Error() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[PUT /companyproducts][%d] putCompanyProductNotFound %s", 404, payload)
}
func (o *PutCompanyProductNotFound) String() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[PUT /companyproducts][%d] putCompanyProductNotFound %s", 404, payload)
}
func (o *PutCompanyProductNotFound) GetPayload() *research_models.Error {
return o.Payload
}
func (o *PutCompanyProductNotFound) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
// hydrates response header Access-Control-Allow-Origin
hdrAccessControlAllowOrigin := response.GetHeader("Access-Control-Allow-Origin")
if hdrAccessControlAllowOrigin != "" {
o.AccessControlAllowOrigin = hdrAccessControlAllowOrigin
}
o.Payload = new(research_models.Error)
// response payload
if err := consumer.Consume(response.Body(), o.Payload); err != nil && !stderrors.Is(err, io.EOF) {
return err
}
return nil
}
// NewPutCompanyProductConflict creates a PutCompanyProductConflict with default headers values
func NewPutCompanyProductConflict() *PutCompanyProductConflict {
return &PutCompanyProductConflict{}
}
// PutCompanyProductConflict describes a response with status code 409, with default header values.
//
// Conflict
type PutCompanyProductConflict struct {
AccessControlAllowOrigin string
Payload *research_models.Error
}
// IsSuccess returns true when this put company product conflict response has a 2xx status code
func (o *PutCompanyProductConflict) IsSuccess() bool {
return false
}
// IsRedirect returns true when this put company product conflict response has a 3xx status code
func (o *PutCompanyProductConflict) IsRedirect() bool {
return false
}
// IsClientError returns true when this put company product conflict response has a 4xx status code
func (o *PutCompanyProductConflict) IsClientError() bool {
return true
}
// IsServerError returns true when this put company product conflict response has a 5xx status code
func (o *PutCompanyProductConflict) IsServerError() bool {
return false
}
// IsCode returns true when this put company product conflict response a status code equal to that given
func (o *PutCompanyProductConflict) IsCode(code int) bool {
return code == 409
}
// Code gets the status code for the put company product conflict response
func (o *PutCompanyProductConflict) Code() int {
return 409
}
func (o *PutCompanyProductConflict) Error() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[PUT /companyproducts][%d] putCompanyProductConflict %s", 409, payload)
}
func (o *PutCompanyProductConflict) String() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[PUT /companyproducts][%d] putCompanyProductConflict %s", 409, payload)
}
func (o *PutCompanyProductConflict) GetPayload() *research_models.Error {
return o.Payload
}
func (o *PutCompanyProductConflict) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
// hydrates response header Access-Control-Allow-Origin
hdrAccessControlAllowOrigin := response.GetHeader("Access-Control-Allow-Origin")
if hdrAccessControlAllowOrigin != "" {
o.AccessControlAllowOrigin = hdrAccessControlAllowOrigin
}
o.Payload = new(research_models.Error)
// response payload
if err := consumer.Consume(response.Body(), o.Payload); err != nil && !stderrors.Is(err, io.EOF) {
return err
}
return nil
}
// NewPutCompanyProductUnprocessableEntity creates a PutCompanyProductUnprocessableEntity with default headers values
func NewPutCompanyProductUnprocessableEntity() *PutCompanyProductUnprocessableEntity {
return &PutCompanyProductUnprocessableEntity{}
}
// PutCompanyProductUnprocessableEntity describes a response with status code 422, with default header values.
//
// Unprocessable Entity, likely a bad parameter
type PutCompanyProductUnprocessableEntity struct {
AccessControlAllowOrigin string
Payload *research_models.Error
}
// IsSuccess returns true when this put company product unprocessable entity response has a 2xx status code
func (o *PutCompanyProductUnprocessableEntity) IsSuccess() bool {
return false
}
// IsRedirect returns true when this put company product unprocessable entity response has a 3xx status code
func (o *PutCompanyProductUnprocessableEntity) IsRedirect() bool {
return false
}
// IsClientError returns true when this put company product unprocessable entity response has a 4xx status code
func (o *PutCompanyProductUnprocessableEntity) IsClientError() bool {
return true
}
// IsServerError returns true when this put company product unprocessable entity response has a 5xx status code
func (o *PutCompanyProductUnprocessableEntity) IsServerError() bool {
return false
}
// IsCode returns true when this put company product unprocessable entity response a status code equal to that given
func (o *PutCompanyProductUnprocessableEntity) IsCode(code int) bool {
return code == 422
}
// Code gets the status code for the put company product unprocessable entity response
func (o *PutCompanyProductUnprocessableEntity) Code() int {
return 422
}
func (o *PutCompanyProductUnprocessableEntity) Error() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[PUT /companyproducts][%d] putCompanyProductUnprocessableEntity %s", 422, payload)
}
func (o *PutCompanyProductUnprocessableEntity) String() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[PUT /companyproducts][%d] putCompanyProductUnprocessableEntity %s", 422, payload)
}
func (o *PutCompanyProductUnprocessableEntity) GetPayload() *research_models.Error {
return o.Payload
}
func (o *PutCompanyProductUnprocessableEntity) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
// hydrates response header Access-Control-Allow-Origin
hdrAccessControlAllowOrigin := response.GetHeader("Access-Control-Allow-Origin")
if hdrAccessControlAllowOrigin != "" {
o.AccessControlAllowOrigin = hdrAccessControlAllowOrigin
}
o.Payload = new(research_models.Error)
// response payload
if err := consumer.Consume(response.Body(), o.Payload); err != nil && !stderrors.Is(err, io.EOF) {
return err
}
return nil
}
// NewPutCompanyProductInternalServerError creates a PutCompanyProductInternalServerError with default headers values
func NewPutCompanyProductInternalServerError() *PutCompanyProductInternalServerError {
return &PutCompanyProductInternalServerError{}
}
// PutCompanyProductInternalServerError describes a response with status code 500, with default header values.
//
// Server Internal Error
type PutCompanyProductInternalServerError struct {
AccessControlAllowOrigin string
Payload *research_models.Error
}
// IsSuccess returns true when this put company product internal server error response has a 2xx status code
func (o *PutCompanyProductInternalServerError) IsSuccess() bool {
return false
}
// IsRedirect returns true when this put company product internal server error response has a 3xx status code
func (o *PutCompanyProductInternalServerError) IsRedirect() bool {
return false
}
// IsClientError returns true when this put company product internal server error response has a 4xx status code
func (o *PutCompanyProductInternalServerError) IsClientError() bool {
return false
}
// IsServerError returns true when this put company product internal server error response has a 5xx status code
func (o *PutCompanyProductInternalServerError) IsServerError() bool {
return true
}
// IsCode returns true when this put company product internal server error response a status code equal to that given
func (o *PutCompanyProductInternalServerError) IsCode(code int) bool {
return code == 500
}
// Code gets the status code for the put company product internal server error response
func (o *PutCompanyProductInternalServerError) Code() int {
return 500
}
func (o *PutCompanyProductInternalServerError) Error() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[PUT /companyproducts][%d] putCompanyProductInternalServerError %s", 500, payload)
}
func (o *PutCompanyProductInternalServerError) String() string {
payload, _ := json.Marshal(o.Payload)
return fmt.Sprintf("[PUT /companyproducts][%d] putCompanyProductInternalServerError %s", 500, payload)
}
func (o *PutCompanyProductInternalServerError) GetPayload() *research_models.Error {
return o.Payload
}
func (o *PutCompanyProductInternalServerError) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
// hydrates response header Access-Control-Allow-Origin
hdrAccessControlAllowOrigin := response.GetHeader("Access-Control-Allow-Origin")
if hdrAccessControlAllowOrigin != "" {
o.AccessControlAllowOrigin = hdrAccessControlAllowOrigin
}
o.Payload = new(research_models.Error)
// response payload
if err := consumer.Consume(response.Body(), o.Payload); err != nil && !stderrors.Is(err, io.EOF) {
return err
}
return nil
}

View File

@ -21,7 +21,7 @@ import (
"github.com/go-openapi/strfmt"
)
const researchProviderSpecSHA256 = "7385c249c9f24d4b1dd4bfd5c1c067c8288bb5b590c72ac9daef5009dc5db0ac"
const researchProviderSpecSHA256 = "1e5b34b5c7b4230cfd507995ab1f837aabc7b4dcdf61b114f37012167afdf224"
type insightCaptureTransport struct {
operation *openapiruntime.ClientOperation

View File

@ -290,7 +290,7 @@ func (a *Client) PostPortfolioEnrichmentRunContext(ctx context.Context, params *
// PostPortfolioReconciliation reconciles one portfolio candidate in discovery mode.
//
// Create exactly one authoritative Product or Service, or compare-and-swap update one authoritative Service, from one immutable direct-write candidate in a completed same-tenant portfolio_enrichment.v1 run. This Discovery-only operation requires an active server-side tenant policy for the exact entity action, the matching create or update scope, an active Owner or Manager session, explicit acknowledgement, and an idempotency key. Operator-selected field values remain distinct from immutable candidate provenance. Existing Product changes remain governed proposals. It never deletes, creates relationships, proposes, reviews, applies, or publishes..
// Create or compare-and-swap update exactly one authoritative Product or Service from one immutable direct-write candidate in a completed same-tenant portfolio_enrichment.v1 run. This Discovery-only operation requires an active server-side tenant policy for the exact entity action, the matching create or update scope, an active Owner or Manager session, explicit acknowledgement, and an idempotency key. Operator-selected field values remain distinct from immutable candidate provenance. Legacy Product governed-update candidates are not silently converted to direct writes and must be rerun. It never deletes, creates relationships, proposes, reviews, applies, or publishes..
//
// This method does not support injected context.
// However, timeout and opentracing contexts are honored whenever enabled.
@ -309,7 +309,7 @@ func (a *Client) PostPortfolioReconciliation(params *PostPortfolioReconciliation
// PostPortfolioReconciliationContext reconciles one portfolio candidate in discovery mode.
//
// Create exactly one authoritative Product or Service, or compare-and-swap update one authoritative Service, from one immutable direct-write candidate in a completed same-tenant portfolio_enrichment.v1 run. This Discovery-only operation requires an active server-side tenant policy for the exact entity action, the matching create or update scope, an active Owner or Manager session, explicit acknowledgement, and an idempotency key. Operator-selected field values remain distinct from immutable candidate provenance. Existing Product changes remain governed proposals. It never deletes, creates relationships, proposes, reviews, applies, or publishes..
// Create or compare-and-swap update exactly one authoritative Product or Service from one immutable direct-write candidate in a completed same-tenant portfolio_enrichment.v1 run. This Discovery-only operation requires an active server-side tenant policy for the exact entity action, the matching create or update scope, an active Owner or Manager session, explicit acknowledgement, and an idempotency key. Operator-selected field values remain distinct from immutable candidate provenance. Legacy Product governed-update candidates are not silently converted to direct writes and must be rerun. It never deletes, creates relationships, proposes, reviews, applies, or publishes..
//
// Do not use the deprecated [PostPortfolioReconciliationParams.Context] with this method: it would be ignored.
func (a *Client) PostPortfolioReconciliationContext(ctx context.Context, params *PostPortfolioReconciliationParams, authInfo runtime.ClientAuthInfoWriter, opts ...ClientOption) (*PostPortfolioReconciliationOK, error) {

View File

@ -182,7 +182,21 @@ func TestGeneratedServiceUpdateReceiptCarriesBeforeAndResultVersions(t *testing.
}
}
func TestPortfolioReconciliationContractKeepsProductUpdatesGoverned(t *testing.T) {
func TestGeneratedProductUpdateReconciliationRequestShape(t *testing.T) {
request := validProductUpdateReconciliationRequest()
if err := request.Validate(strfmt.Default); err != nil {
t.Fatalf("valid Product update request failed generated validation: %v", err)
}
if got := *request.Action; got != research_models.PortfolioReconciliationRequestActionUpdate {
t.Errorf("Action = %q, want update", got)
}
if got := *request.EntityType; got !=
research_models.PortfolioReconciliationRequestEntityTypeCompanyProduct {
t.Errorf("EntityType = %q, want company_product", got)
}
}
func TestPortfolioReconciliationSupportsProductUpdatesWithoutConvertingLegacyRuns(t *testing.T) {
document, err := loads.Spec(filepath.Join(
portfolioReconciliationRepoRoot(t), "swagger", "research-vernonkeenan.yaml",
))
@ -195,15 +209,21 @@ func TestPortfolioReconciliationContractKeepsProductUpdatesGoverned(t *testing.T
}
if !strings.Contains(
reconciliation.Post.Description,
"Existing Product changes remain governed proposals",
"Create or compare-and-swap update exactly one authoritative Product or Service",
) {
t.Fatal("reconciliation contract does not preserve Product update rejection")
t.Fatal("reconciliation contract does not expose Product direct update")
}
if document.Spec().Paths.Paths["/companyproducts"].Put != nil {
t.Fatal("provider projection exposes a direct CompanyProduct PUT")
if !strings.Contains(
reconciliation.Post.Description,
"Legacy Product governed-update candidates are not silently converted to direct writes",
) {
t.Fatal("reconciliation contract does not keep legacy Product runs fail-closed")
}
if document.Spec().Paths.Paths["/companyproducts"].Put == nil {
t.Fatal("provider projection is missing direct CompanyProduct PUT")
}
if _, ok := reconciliation.Post.Responses.StatusCodeResponses[422]; !ok {
t.Fatal("reconciliation contract does not expose invalid Product update rejection")
t.Fatal("reconciliation contract does not expose invalid or legacy Product update rejection")
}
if !portfolio_enrichment_runs.
NewPostPortfolioReconciliationUnprocessableEntity().
@ -256,6 +276,15 @@ func validServiceUpdateReconciliationRequest() *research_models.PortfolioReconci
}
}
func validProductUpdateReconciliationRequest() *research_models.PortfolioReconciliationRequest {
request := validServiceUpdateReconciliationRequest()
entityType := research_models.PortfolioReconciliationRequestEntityTypeCompanyProduct
idempotencyKey := "studio-product-update-0001"
request.EntityType = &entityType
request.IdempotencyKey = &idempotencyKey
return request
}
func portfolioReconciliationRepoRoot(t *testing.T) string {
t.Helper()
_, testFile, _, ok := runtime.Caller(0)

View File

@ -1,9 +1,9 @@
# Research company catalog client
This slice synchronizes the full Research provider specification from
`vernonkeenan/research` draft PR #32 at provider commit
`ca6a2911ce79d053f9aa338e6d9bcfe3357b0c77`. The pinned internal spec SHA-256
is `c213fe19077453fcdc8f8ce3f6d1671cbbb5074f348c9e4ee0739f64ab0ebba3`;
This slice synchronizes the full Research provider specification from PR #57
source commit `c3127f32872051b842c74ae7139b1c6ef71e2c53`, merged to Research `main`
as `cc7bf6191dc105969c804e123823186f1ea26cc3`. The pinned internal spec
SHA-256 is `1e5b34b5c7b4230cfd507995ab1f837aabc7b4dcdf61b114f37012167afdf224`;
the external copy differs only by Lib's conventional scheme, host, and base
path rewrite.
@ -11,7 +11,9 @@ The generated CompanyProduct surface contains:
- shared list/detail through `GET /companyproducts`;
- safe tenant-attributed create through `POST /companyproducts`; and
- no direct update or delete.
- optimistic-concurrency update of unpublished Products through
`PUT /companyproducts`; and
- no delete.
The generated CompanyService surface contains:
@ -26,26 +28,33 @@ Every operation retains one compound security alternative containing both
forwards only the `kvSession` cookie. Credential values do not enter the
generated models, source, tests, or logs.
Product update remains intentionally proposal-only. The separate generated
proposal client records, submits, and independently reviews Product changes;
review does not itself apply or publish the authoritative Product. This Lib
Published Product update remains intentionally proposal-only. The separate
generated proposal client records, submits, and independently reviews Product
changes; review does not itself apply or publish the authoritative Product.
Discovery reconciliation may update an unpublished Product, but legacy
governed-update candidates remain fail-closed and must be rerun rather than
silently converted. The Product PUT contract preserves immutable Account
binding, publication state, enrichment audit, and media ownership. This Lib
slice does not invent application, publication, bulk, media-ownership, or
delete operations.
## Dependency and release gate
This Lib PR may be reviewed while Research PR #32 is open, but it must not be
released until:
This Lib PR may be reviewed while the Research Product-update change is open,
but it must not be released until:
1. Research PR #32 is merged, released, and deployed;
1. Research merge `cc7bf6191dc105969c804e123823186f1ea26cc3` is
released and deployed;
2. the KV Studio native service principal has the exact governed scopes
`research:company-product:read`, `research:company-product:create`,
`research:company-product:update`,
`research:company-service:read`, `research:company-service:create`, and
`research:company-service:update`;
3. Research production smoke tests prove the native catalog paths and zero
Salesforce reads or writes; and
4. KV Studio is ready to pin the new Lib tag, preserve the Product proposal
workflow, and run its Playwright list/detail/create/Service-edit coverage.
workflow for governed and published changes, and run its Playwright
list/detail/create/Product-edit/Service-edit coverage.
There is no database migration in this client slice. Research remains
responsible for proving ownership through the existing

View File

@ -585,7 +585,7 @@ paths:
tags:
- CompanyProducts
post:
description: Create one unpublished CompanyProduct beneath an Account owned by the machine tenant; requires research:company-product:create and same-tenant Owner or Manager. Existing Product updates remain proposal-only.
description: Create one unpublished CompanyProduct beneath an Account owned by the machine tenant; requires research:company-product:create and same-tenant Owner or Manager
operationId: postCompanyProducts
parameters:
- $ref: "#/parameters/companyProductRequest"
@ -608,6 +608,32 @@ paths:
summary: Create one tenant-owned CompanyProduct
tags:
- CompanyProducts
put:
description: Replace the editable fields of one shared-catalog CompanyProduct using optimistic concurrency while preserving its immutable Account binding, publication state, enrichment audit, and media ownership. Published Products require the governed proposal workflow. Requires research:company-product:update and a same-tenant Owner or Manager.
operationId: putCompanyProduct
parameters:
- $ref: "#/parameters/companyProductRequest"
responses:
"200":
$ref: "#/responses/CompanyProductResponse"
"401":
$ref: "#/responses/Unauthorized"
"403":
$ref: "#/responses/AccessForbidden"
"404":
$ref: "#/responses/NotFound"
"409":
$ref: "#/responses/Conflict"
"422":
$ref: "#/responses/UnprocessableEntity"
"500":
$ref: "#/responses/ServerError"
security:
- ApiKeyAuth: []
kvSessionCookie: []
summary: Update one unpublished shared-catalog CompanyProduct
tags:
- CompanyProducts
/publishedcompanyproducts:
get:
description: Return only published CompanyProducts whose owning CRM Account is also published; requires a tenant-bound native service principal with the exact research:company-product:public-read scope and does not accept a human session
@ -1008,7 +1034,7 @@ paths:
- PortfolioEnrichmentRuns
/portfolioenrichmentruns/{portfolioEnrichmentRunId}/reconciliations:
post:
description: Create exactly one authoritative Product or Service, or compare-and-swap update one authoritative Service, from one immutable direct-write candidate in a completed same-tenant portfolio_enrichment.v1 run. This Discovery-only operation requires an active server-side tenant policy for the exact entity action, the matching create or update scope, an active Owner or Manager session, explicit acknowledgement, and an idempotency key. Operator-selected field values remain distinct from immutable candidate provenance. Existing Product changes remain governed proposals. It never deletes, creates relationships, proposes, reviews, applies, or publishes.
description: Create or compare-and-swap update exactly one authoritative Product or Service from one immutable direct-write candidate in a completed same-tenant portfolio_enrichment.v1 run. This Discovery-only operation requires an active server-side tenant policy for the exact entity action, the matching create or update scope, an active Owner or Manager session, explicit acknowledgement, and an idempotency key. Operator-selected field values remain distinct from immutable candidate provenance. Legacy Product governed-update candidates are not silently converted to direct writes and must be rerun. It never deletes, creates relationships, proposes, reviews, applies, or publishes.
operationId: postPortfolioReconciliation
parameters:
- $ref: "#/parameters/portfolioEnrichmentRunIdPath"

View File

@ -585,7 +585,7 @@ paths:
tags:
- CompanyProducts
post:
description: Create one unpublished CompanyProduct beneath an Account owned by the machine tenant; requires research:company-product:create and same-tenant Owner or Manager. Existing Product updates remain proposal-only.
description: Create one unpublished CompanyProduct beneath an Account owned by the machine tenant; requires research:company-product:create and same-tenant Owner or Manager
operationId: postCompanyProducts
parameters:
- $ref: "#/parameters/companyProductRequest"
@ -608,6 +608,32 @@ paths:
summary: Create one tenant-owned CompanyProduct
tags:
- CompanyProducts
put:
description: Replace the editable fields of one shared-catalog CompanyProduct using optimistic concurrency while preserving its immutable Account binding, publication state, enrichment audit, and media ownership. Published Products require the governed proposal workflow. Requires research:company-product:update and a same-tenant Owner or Manager.
operationId: putCompanyProduct
parameters:
- $ref: "#/parameters/companyProductRequest"
responses:
"200":
$ref: "#/responses/CompanyProductResponse"
"401":
$ref: "#/responses/Unauthorized"
"403":
$ref: "#/responses/AccessForbidden"
"404":
$ref: "#/responses/NotFound"
"409":
$ref: "#/responses/Conflict"
"422":
$ref: "#/responses/UnprocessableEntity"
"500":
$ref: "#/responses/ServerError"
security:
- ApiKeyAuth: []
kvSessionCookie: []
summary: Update one unpublished shared-catalog CompanyProduct
tags:
- CompanyProducts
/publishedcompanyproducts:
get:
description: Return only published CompanyProducts whose owning CRM Account is also published; requires a tenant-bound native service principal with the exact research:company-product:public-read scope and does not accept a human session
@ -1008,7 +1034,7 @@ paths:
- PortfolioEnrichmentRuns
/portfolioenrichmentruns/{portfolioEnrichmentRunId}/reconciliations:
post:
description: Create exactly one authoritative Product or Service, or compare-and-swap update one authoritative Service, from one immutable direct-write candidate in a completed same-tenant portfolio_enrichment.v1 run. This Discovery-only operation requires an active server-side tenant policy for the exact entity action, the matching create or update scope, an active Owner or Manager session, explicit acknowledgement, and an idempotency key. Operator-selected field values remain distinct from immutable candidate provenance. Existing Product changes remain governed proposals. It never deletes, creates relationships, proposes, reviews, applies, or publishes.
description: Create or compare-and-swap update exactly one authoritative Product or Service from one immutable direct-write candidate in a completed same-tenant portfolio_enrichment.v1 run. This Discovery-only operation requires an active server-side tenant policy for the exact entity action, the matching create or update scope, an active Owner or Manager session, explicit acknowledgement, and an idempotency key. Operator-selected field values remain distinct from immutable candidate provenance. Legacy Product governed-update candidates are not silently converted to direct writes and must be rerun. It never deletes, creates relationships, proposes, reviews, applies, or publishes.
operationId: postPortfolioReconciliation
parameters:
- $ref: "#/parameters/portfolioEnrichmentRunIdPath"