Files
cgalo5758 fc68a6b78e Stop offering "None" for the entitlement set of a product that is a tier
Fourth section of #153's change. The product edit reads the ladders the
product is a tier of. For a tier product the Entitlement set select no
longer offers "None"; it opens with the disabled placeholder "Choose an
entitlement set", and the field's one line reads "This product is a tier
of <ladders>, and a tier needs an entitlement set." in place of the
off-ladder sentence, which moves from the field's declared hint to the
same line slot (the create page keeps it). A submitted "None" is refused
with that sentence; so is a clear refused by section 2's trigger when
the product became a tier after the page was read, after the ladders are
read again. A failed read of the ladders is logged and saves nothing,
instead of reading as "on no ladder".

The edit's write now runs in its own READ COMMITTED transaction
(db.InTx with db.ReadCommitted, item 4.6), so a tier add that commits
first is seen by the clear whatever the server's default isolation; at
REPEATABLE READ the edit's snapshot predated the tier and the clear
committed. UpdateProduct is split into helpers, each answer and log line
kept.

Tests: TestTheEntitlementSetFieldFollowsTheProductsTiers,
TestTheCreatePageCarriesTheOffLadderLine, TestTierNeedsSetSentence,
TestProductEditOfATierProductOffersNoNone,
TestProductEditRefusesNoneForAProductThatBecameATier,
TestProductEditWithUnreadLaddersSavesNothing,
TestProductEditClearingASetIsRefusedWhenATierAddCommitsFirstAtAnyDefaultIsolation
(on a connection whose default is REPEATABLE READ). The forms golden
operator.product.html moves the hint to the line.
TestUpdateProductEntitlementSetSentinel builds its product on committed
rows, since the edit's own transaction cannot see an uncommitted
fixture; its assertions are unchanged. productTierOf takes no context:
it logs through the handler's logger and unparam refuses the unused
parameter.

Breaks: 20 of 20 caught. The parse's error left in place; None offered
whatever the tiers (T6, T6c, T6d); the placeholder left out (T6d, T6);
the ladders out of name order; the line kept beside the error; the
off-ladder line blanked beside an error; the create page's line lost;
the failed read treated as no tier; the save going ahead when the read
failed; the parse's field error left when the ladders are unread; L3 not
logged; the second-read refusal not logged; the database's refusal
unmapped; the refusal named over a failed second read; the join reduced
to the first name; the write at the server's default isolation; the
empty-value condition dropped.

Review: round 1, no blocker or major; four minor and three nits,
applied.

#153's sequence (item 5.4), app rebuilt from this tree, from the demo
snapshot, the whole walkthrough package three times without a reset: ok
each run. The plan-ladders walkthrough's ladder now has tiers with sets,
the org-types walkthrough committed a default change to it on runs 2
and 3 and put the default back, and the grant revoke passed on all three
runs; the app log has no "has no entitlement set". Item 5.4 is reworded
to what ran: the three-walkthrough filter it named finds no revoke
subject on the demo snapshot, which another walkthrough's grant
supplies.

Full suite on reset databases: 4731 pass, 14 skip. make screens: no
change beyond section 1's; the captured product is not a tier.
2026-10-26 19:02:13 -05:00

681 lines
35 KiB
HTML

===== operator.product unbound
<div class="card app-form-box mb-3"><div class="card-body">
<form id="form-operator.product" data-form="operator.product" data-form-kind="create" class="app-form" novalidate hx-post="/partials/operator/products" hx-target="#operator-body" hx-swap="innerHTML" hx-disable="find button[type=submit]" hx-indicator="find .htmx-indicator">
<div id="form-operator.product-error" class="app-form-error" role="alert"></div>
<div class="app-form-fields">
<div id="form-operator.product-name" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-name-control">Name</label>
<input type="text" class="form-control" id="form-operator.product-name-control" name="name" value="" required maxlength="200" autocomplete="off" aria-describedby="form-operator.product-name-warning" hx-get="/partials/operator/products/name-check" hx-include="closest form" hx-swap="innerHTML" hx-target="#form-operator\.product-name-warning" hx-trigger="input changed delay:300ms">
<div id="form-operator.product-name-warning"></div>
</div><div id="form-operator.product-display_category" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-display_category-control">Display category <span class="text-muted">(optional)</span></label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Display category" data-bs-content="Grouping for the storefront and operator lists; never drives behavior. Blank is fine." aria-label="Help: Display category"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
<input type="text" class="form-control" id="form-operator.product-display_category-control" name="display_category" value="" placeholder="e.g. addon" maxlength="100" autocomplete="off">
</div><div id="form-operator.product-entitlement_set_id" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-entitlement_set_id-control">Entitlement set</label>
<select class="form-select" id="form-operator.product-entitlement_set_id-control" name="entitlement_set_id" required aria-describedby="form-operator.product-entitlement_set_id-line">
<option value="" disabled selected>Choose one</option>
<option value="a">Alpha</option>
<option value="b">Beta</option>
</select>
<div id="form-operator.product-entitlement_set_id-line"></div>
</div><div id="form-operator.product-visibility" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-visibility-control">Listed</label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Listed" data-bs-content="Shown in the member catalog." aria-label="Help: Listed"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
<div class="app-form-check-control"><input class="form-check-input" type="checkbox" id="form-operator.product-visibility-control" name="visibility" value="public" ></div>
</div><div id="form-operator.product-description" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-description-control">Description <span class="text-muted">(optional)</span></label>
<input type="text" class="form-control" id="form-operator.product-description-control" name="description" value="" maxlength="500" autocomplete="off">
</div><div id="form-operator.product-features" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-features-control">Additional features <span class="text-muted">(optional)</span></label>
<textarea class="form-control" id="form-operator.product-features-control" name="features" rows="3" maxlength="2000" aria-describedby="form-operator.product-features-hint"></textarea>
<div id="form-operator.product-features-hint" class="form-text">Shown on the Products page, one per line; supplements entitlement-derived limits.</div>
</div><div id="form-operator.product-is_active" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-is_active-control">Active</label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Active" data-bs-content="An inactive product is not offered when new grants are created. Grants that already reference it keep working." aria-label="Help: Active"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
<div class="app-form-check-control"><input class="form-check-input" type="checkbox" id="form-operator.product-is_active-control" name="is_active" value="true" ></div>
</div><input type="hidden" name="exclude" value="" >
</div>
<div class="d-flex flex-wrap gap-2 app-form-commit">
<button type="submit" class="btn btn-primary"><span class="spinner-border spinner-border-sm htmx-indicator me-1" aria-hidden="true"></span>Create product</button>
<a href="/operator/products" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div></div>
===== operator.product unbound edit side
<div class="card app-form-box mb-3"><div class="card-body">
<form id="form-operator.product" data-form="operator.product" data-form-kind="edit" class="app-form" novalidate hx-put="/partials/operator/products/{productID}" hx-target="#operator-body" hx-swap="innerHTML" hx-disable="find button[type=submit]" hx-indicator="find .htmx-indicator">
<div id="form-operator.product-error" class="app-form-error" role="alert"></div>
<div class="app-form-fields">
<div id="form-operator.product-name" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-name-control">Name</label>
<input type="text" class="form-control" id="form-operator.product-name-control" name="name" value="" required maxlength="200" autocomplete="off" aria-describedby="form-operator.product-name-warning" hx-get="/partials/operator/products/name-check" hx-include="closest form" hx-swap="innerHTML" hx-target="#form-operator\.product-name-warning" hx-trigger="input changed delay:300ms">
<div id="form-operator.product-name-warning"></div>
</div><div id="form-operator.product-display_category" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-display_category-control">Display category <span class="text-muted">(optional)</span></label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Display category" data-bs-content="Grouping for the storefront and operator lists; never drives behavior. Blank is fine." aria-label="Help: Display category"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
<input type="text" class="form-control" id="form-operator.product-display_category-control" name="display_category" value="" placeholder="e.g. addon" maxlength="100" autocomplete="off">
</div><div id="form-operator.product-entitlement_set_id" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-entitlement_set_id-control">Entitlement set</label>
<select class="form-select" id="form-operator.product-entitlement_set_id-control" name="entitlement_set_id" required aria-describedby="form-operator.product-entitlement_set_id-line">
<option value="" disabled selected>Choose one</option>
<option value="a">Alpha</option>
<option value="b">Beta</option>
</select>
<div id="form-operator.product-entitlement_set_id-line"></div>
</div><div id="form-operator.product-visibility" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-visibility-control">Listed</label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Listed" data-bs-content="Shown in the member catalog." aria-label="Help: Listed"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
<div class="app-form-check-control"><input class="form-check-input" type="checkbox" id="form-operator.product-visibility-control" name="visibility" value="public" ></div>
</div><div id="form-operator.product-description" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-description-control">Description <span class="text-muted">(optional)</span></label>
<input type="text" class="form-control" id="form-operator.product-description-control" name="description" value="" maxlength="500" autocomplete="off">
</div><div id="form-operator.product-features" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-features-control">Additional features <span class="text-muted">(optional)</span></label>
<textarea class="form-control" id="form-operator.product-features-control" name="features" rows="3" maxlength="2000" aria-describedby="form-operator.product-features-hint"></textarea>
<div id="form-operator.product-features-hint" class="form-text">Shown on the Products page, one per line; supplements entitlement-derived limits.</div>
</div><div id="form-operator.product-is_active" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-is_active-control">Active</label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Active" data-bs-content="An inactive product is not offered when new grants are created. Grants that already reference it keep working." aria-label="Help: Active"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
<div class="app-form-check-control"><input class="form-check-input" type="checkbox" id="form-operator.product-is_active-control" name="is_active" value="true" ></div>
</div><input type="hidden" name="exclude" value="" >
</div>
<div class="d-flex flex-wrap gap-2 app-form-commit">
<button type="submit" class="btn btn-primary"><span class="spinner-border spinner-border-sm htmx-indicator me-1" aria-hidden="true"></span>Save changes</button>
</div>
</form>
</div></div>
===== operator.product create side
<div class="card app-form-box mb-3"><div class="card-body">
<form id="form-operator.product" data-form="operator.product" data-form-kind="create" class="app-form" novalidate hx-post="/partials/operator/products" hx-target="#operator-body" hx-swap="innerHTML" hx-disable="find button[type=submit]" hx-indicator="find .htmx-indicator">
<div id="form-operator.product-error" class="app-form-error" role="alert"></div>
<div class="app-form-fields">
<div id="form-operator.product-name" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-name-control">Name</label>
<input type="text" class="form-control" id="form-operator.product-name-control" name="name" value="" required maxlength="200" autocomplete="off" aria-describedby="form-operator.product-name-warning" hx-get="/partials/operator/products/name-check" hx-include="closest form" hx-swap="innerHTML" hx-target="#form-operator\.product-name-warning" hx-trigger="input changed delay:300ms">
<div id="form-operator.product-name-warning"></div>
</div><div id="form-operator.product-display_category" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-display_category-control">Display category <span class="text-muted">(optional)</span></label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Display category" data-bs-content="Grouping for the storefront and operator lists; never drives behavior. Blank is fine." aria-label="Help: Display category"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
<input type="text" class="form-control" id="form-operator.product-display_category-control" name="display_category" value="" placeholder="e.g. addon" maxlength="100" autocomplete="off">
</div><div id="form-operator.product-entitlement_set_id" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-entitlement_set_id-control">Entitlement set</label>
<select class="form-select" id="form-operator.product-entitlement_set_id-control" name="entitlement_set_id" required aria-describedby="form-operator.product-entitlement_set_id-line">
<option value="" disabled selected>Choose one</option>
<option value="a">Alpha</option>
<option value="b">Beta</option>
</select>
<div id="form-operator.product-entitlement_set_id-line"></div>
</div><div id="form-operator.product-visibility" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-visibility-control">Listed</label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Listed" data-bs-content="Shown in the member catalog." aria-label="Help: Listed"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
<div class="app-form-check-control"><input class="form-check-input" type="checkbox" id="form-operator.product-visibility-control" name="visibility" value="public" ></div>
</div>
</div>
<div class="d-flex flex-wrap gap-2 app-form-commit">
<button type="submit" class="btn btn-primary"><span class="spinner-border spinner-border-sm htmx-indicator me-1" aria-hidden="true"></span>Create product</button>
<a href="/operator/products" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div></div>
===== operator.product unbound with values
<div class="card app-form-box mb-3"><div class="card-body">
<form id="form-operator.product" data-form="operator.product" data-form-kind="create" class="app-form" novalidate hx-post="/partials/operator/products" hx-target="#operator-body" hx-swap="innerHTML" hx-disable="find button[type=submit]" hx-indicator="find .htmx-indicator">
<div id="form-operator.product-error" class="app-form-error" role="alert"></div>
<div class="app-form-fields">
<div id="form-operator.product-name" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-name-control">Name</label>
<input type="text" class="form-control" id="form-operator.product-name-control" name="name" value="" required maxlength="200" autocomplete="off" aria-describedby="form-operator.product-name-warning" hx-get="/partials/operator/products/name-check" hx-include="closest form" hx-swap="innerHTML" hx-target="#form-operator\.product-name-warning" hx-trigger="input changed delay:300ms">
<div id="form-operator.product-name-warning"></div>
</div><div id="form-operator.product-display_category" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-display_category-control">Display category <span class="text-muted">(optional)</span></label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Display category" data-bs-content="Grouping for the storefront and operator lists; never drives behavior. Blank is fine." aria-label="Help: Display category"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
<input type="text" class="form-control" id="form-operator.product-display_category-control" name="display_category" value="" placeholder="e.g. addon" maxlength="100" autocomplete="off">
</div><div id="form-operator.product-entitlement_set_id" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-entitlement_set_id-control">Entitlement set</label>
<select class="form-select" id="form-operator.product-entitlement_set_id-control" name="entitlement_set_id" required aria-describedby="form-operator.product-entitlement_set_id-line">
<option value="" disabled selected>Choose one</option>
<option value="a">Alpha</option>
<option value="b">Beta</option>
</select>
<div id="form-operator.product-entitlement_set_id-line"></div>
</div><div id="form-operator.product-visibility" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-visibility-control">Listed</label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Listed" data-bs-content="Shown in the member catalog." aria-label="Help: Listed"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
<div class="app-form-check-control"><input class="form-check-input" type="checkbox" id="form-operator.product-visibility-control" name="visibility" value="public" ></div>
</div><div id="form-operator.product-description" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-description-control">Description <span class="text-muted">(optional)</span></label>
<input type="text" class="form-control" id="form-operator.product-description-control" name="description" value="" maxlength="500" autocomplete="off">
</div><div id="form-operator.product-features" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-features-control">Additional features <span class="text-muted">(optional)</span></label>
<textarea class="form-control" id="form-operator.product-features-control" name="features" rows="3" maxlength="2000" aria-describedby="form-operator.product-features-hint"></textarea>
<div id="form-operator.product-features-hint" class="form-text">Shown on the Products page, one per line; supplements entitlement-derived limits.</div>
</div><div id="form-operator.product-is_active" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-is_active-control">Active</label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Active" data-bs-content="An inactive product is not offered when new grants are created. Grants that already reference it keep working." aria-label="Help: Active"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
<div class="app-form-check-control"><input class="form-check-input" type="checkbox" id="form-operator.product-is_active-control" name="is_active" value="true" ></div>
</div><input type="hidden" name="exclude" value="sample" >
</div>
<div class="d-flex flex-wrap gap-2 app-form-commit">
<button type="submit" class="btn btn-primary"><span class="spinner-border spinner-border-sm htmx-indicator me-1" aria-hidden="true"></span>Create product</button>
<a href="/operator/products" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div></div>
===== operator.product record
<div class="card app-form-box mb-3"><div class="card-body">
<form id="form-operator.product-i1" data-form="operator.product" data-form-kind="create" class="app-form" novalidate hx-post="/partials/operator/products" hx-target="#operator-body" hx-swap="innerHTML" hx-disable="find button[type=submit]" hx-indicator="find .htmx-indicator">
<div id="form-operator.product-i1-error" class="app-form-error" role="alert"></div>
<div class="app-form-message mb-2"><p>msg</p></div>
<div class="app-form-fields">
<div id="form-operator.product-i1-name" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-i1-name-control">Name</label>
<input type="text" class="form-control" id="form-operator.product-i1-name-control" name="name" value="sample" required maxlength="200" autocomplete="off" aria-describedby="form-operator.product-i1-name-warning" hx-get="/partials/operator/products/name-check" hx-include="closest form" hx-swap="innerHTML" hx-target="#form-operator\.product-name-warning" hx-trigger="input changed delay:300ms">
<div id="form-operator.product-i1-name-warning"></div>
</div><div id="form-operator.product-i1-display_category" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-i1-display_category-control">Display category <span class="text-muted">(optional)</span></label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Display category" data-bs-content="Grouping for the storefront and operator lists; never drives behavior. Blank is fine." aria-label="Help: Display category"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
<input type="text" class="form-control" id="form-operator.product-i1-display_category-control" name="display_category" value="sample" placeholder="e.g. addon" maxlength="100" autocomplete="off">
</div><div id="form-operator.product-i1-entitlement_set_id" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-i1-entitlement_set_id-control">Entitlement set</label>
<select class="form-select" id="form-operator.product-i1-entitlement_set_id-control" name="entitlement_set_id" required aria-describedby="form-operator.product-i1-entitlement_set_id-line">
<option value="" disabled>Choose one</option>
<option value="a" selected>Alpha</option>
<option value="b">Beta</option>
</select>
<div id="form-operator.product-i1-entitlement_set_id-line"></div>
</div><div id="form-operator.product-i1-visibility" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-i1-visibility-control">Listed</label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Listed" data-bs-content="Shown in the member catalog." aria-label="Help: Listed"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
<div class="app-form-check-control"><input class="form-check-input" type="checkbox" id="form-operator.product-i1-visibility-control" name="visibility" value="public" checked ></div>
</div><div id="form-operator.product-i1-description" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-i1-description-control">Description <span class="text-muted">(optional)</span></label>
<input type="text" class="form-control" id="form-operator.product-i1-description-control" name="description" value="sample" maxlength="500" autocomplete="off">
</div><div id="form-operator.product-i1-features" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-i1-features-control">Additional features <span class="text-muted">(optional)</span></label>
<textarea class="form-control" id="form-operator.product-i1-features-control" name="features" rows="3" maxlength="2000" aria-describedby="form-operator.product-i1-features-hint">sample</textarea>
<div id="form-operator.product-i1-features-hint" class="form-text">Shown on the Products page, one per line; supplements entitlement-derived limits.</div>
</div><div id="form-operator.product-i1-is_active" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-i1-is_active-control">Active</label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Active" data-bs-content="An inactive product is not offered when new grants are created. Grants that already reference it keep working." aria-label="Help: Active"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
<div class="app-form-check-control"><input class="form-check-input" type="checkbox" id="form-operator.product-i1-is_active-control" name="is_active" value="true" checked ></div>
</div><input type="hidden" name="exclude" value="sample" >
</div>
<div class="d-flex flex-wrap gap-2 app-form-commit">
<button type="submit" class="btn btn-primary"><span class="spinner-border spinner-border-sm htmx-indicator me-1" aria-hidden="true"></span>Create product</button>
<a href="/operator/products" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div></div>
===== operator.product refused [entitlement_set_id name]
<div class="card app-form-box mb-3"><div class="card-body">
<form id="form-operator.product" data-form="operator.product" data-form-kind="create" class="app-form" novalidate hx-post="/partials/operator/products" hx-target="#operator-body" hx-swap="innerHTML" hx-disable="find button[type=submit]" hx-indicator="find .htmx-indicator">
<div id="form-operator.product-error" class="app-form-error" role="alert"></div>
<div class="app-form-fields">
<div id="form-operator.product-name" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-name-control">Name</label>
<input type="text" class="form-control is-invalid" id="form-operator.product-name-control" name="name" value="" required maxlength="200" autocomplete="off" aria-describedby="form-operator.product-name-error form-operator.product-name-warning" aria-invalid="true" hx-get="/partials/operator/products/name-check" hx-include="closest form" hx-swap="innerHTML" hx-target="#form-operator\.product-name-warning" hx-trigger="input changed delay:300ms" autofocus>
<div id="form-operator.product-name-warning"></div>
<div id="form-operator.product-name-error" class="invalid-feedback d-block">Enter a name.</div>
</div><div id="form-operator.product-display_category" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-display_category-control">Display category <span class="text-muted">(optional)</span></label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Display category" data-bs-content="Grouping for the storefront and operator lists; never drives behavior. Blank is fine." aria-label="Help: Display category"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
<input type="text" class="form-control" id="form-operator.product-display_category-control" name="display_category" value="" placeholder="e.g. addon" maxlength="100" autocomplete="off">
</div><div id="form-operator.product-entitlement_set_id" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-entitlement_set_id-control">Entitlement set</label>
<select class="form-select is-invalid" id="form-operator.product-entitlement_set_id-control" name="entitlement_set_id" required aria-describedby="form-operator.product-entitlement_set_id-error form-operator.product-entitlement_set_id-line" aria-invalid="true">
<option value="" disabled selected>Choose one</option>
<option value="a">Alpha</option>
<option value="b">Beta</option>
</select>
<div id="form-operator.product-entitlement_set_id-line"></div>
<div id="form-operator.product-entitlement_set_id-error" class="invalid-feedback d-block">Choose an entitlement set.</div>
</div><div id="form-operator.product-visibility" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-visibility-control">Listed</label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Listed" data-bs-content="Shown in the member catalog." aria-label="Help: Listed"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
<div class="app-form-check-control"><input class="form-check-input" type="checkbox" id="form-operator.product-visibility-control" name="visibility" value="public" ></div>
</div><div id="form-operator.product-description" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-description-control">Description <span class="text-muted">(optional)</span></label>
<input type="text" class="form-control" id="form-operator.product-description-control" name="description" value="" maxlength="500" autocomplete="off">
</div><div id="form-operator.product-features" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-features-control">Additional features <span class="text-muted">(optional)</span></label>
<textarea class="form-control" id="form-operator.product-features-control" name="features" rows="3" maxlength="2000" aria-describedby="form-operator.product-features-hint"></textarea>
<div id="form-operator.product-features-hint" class="form-text">Shown on the Products page, one per line; supplements entitlement-derived limits.</div>
</div><div id="form-operator.product-is_active" class="mb-3 app-form-field">
<label class="form-label" for="form-operator.product-is_active-control">Active</label> <button type="button" class="btn btn-link p-0 border-0 align-baseline app-help-icon" data-bs-toggle="popover" data-bs-trigger="click" data-bs-title="Active" data-bs-content="An inactive product is not offered when new grants are created. Grants that already reference it keep working." aria-label="Help: Active"><svg class="form-help-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></button>
<div class="app-form-check-control"><input class="form-check-input" type="checkbox" id="form-operator.product-is_active-control" name="is_active" value="true" ></div>
</div><input type="hidden" name="exclude" value="" >
</div>
<div class="d-flex flex-wrap gap-2 app-form-commit">
<button type="submit" class="btn btn-primary"><span class="spinner-border spinner-border-sm htmx-indicator me-1" aria-hidden="true"></span>Create product</button>
<a href="/operator/products" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div></div>
===== operator.product bad-values refusals
description: Description must be 500 characters or fewer.
display_category: Display category must be 100 characters or fewer.
entitlement_set_id: Choose an entitlement set from the list.
name: Name must be 200 characters or fewer.