MR-079 · 080 · 081 · 082 · 083Sales channel account · the recordConnection states
goes to another screengoes to another state of this screenfaded = not mocked yetEverything else — the switch, the ladder, the pickers — works, it just stays on this screen. The URL always names the state you are on.
Yours to set. envisia_main is the provider’s and cannot be edited here.
Connection
Connected
What this stops right now
3active listings on this account1job blocked at push
Money rules
every margin in the listing builder
%A row below this warns amber and still lists.
%Charged by the channel on every sale.
%Applied only when a job promotes the listing.
Margin is price − product cost − shipping − final value fee − ad fee, shipping being postage plus packaging material.
Listing seeds
copied into a job at its startChanging these changes the next job, never a listing that exists.
Operating defaults
synced 2026-08-28 ·
synced 2026-08-28 ·
Return policy has drifted. eBay now calls it Standard returns — 30 day; we hold Standard 30-day returns. Same policy ID.
Ladder models
shared library — a size and its tier are one record
Standard
used by 2 accounts
Preview$
$
Pack
Discount
Pack price
Per unit
Gross margin
Gross margin is price less cost only. It is not the 35% margin floor above — that one is net, and also takes off shipping, the channel fee and the ad rate (D21). Combos take the ladder by bottle count; a size with no tier of its own borrows the deepest tier at or below it, and the listing builder names which.
This account uses Standard.
No operating defaults for a Shopify account
A storefront has no condition, handling time or marketplace policy to copy. Its package set is the store’s variant structure, set on the listing job.
Listings on this account
26Read-only. A listing never moves between accounts.
Listing
SKU
Type
Price
State
Design notes — sales channel account
MR-079 · record half · spec references/ebay-planning/SALES-CHANNEL-ACCOUNT.md
The connection is a machine item, so it is a card
MR-066 established the split on catalog-product-setup.html: items waiting on a person are binary and belong in a table; items waiting on a machine run a lifecycle and belong in cards, because a cell cannot hold a lifecycle. This is that card’s second instance, applied to a channel gate rather than a catalog one — the first evidence for or against MR-066’s claim that every later gate screen reuses both the surface and the split.
Four beats: checked (when we last pinged) · authorised (when the grant was made and when it expires) · scopes (granted against an adapter-supplied required set — eBay’s four are not Shopify’s) · proved (a real read-only call came back). The fourth beat is the difference between we hold a token and it works, and it is the one a naive design leaves out.
Five states, one set of nodes
Connected · expiring · scope gap · lost · never connected are rendered from one fixture through one set of DOM nodes, never five copies — so no two states can drift into describing different screens. The harness strip and the URL name which.
scope gap is the state with no precedent. The token is valid and a required scope is missing, so some calls succeed and others fail. It is not lost because the recovery is a re-grant rather than a re-auth, and the failures it causes are selective rather than total.
First-auth and reconnect are the same card.Connect on a never-connected account and Reconnect on a lost one start the identical handoff — which is why create (MR-070’s pattern: the record returns, the slow half follows) does not need an OAuth step of its own.
Save and Reconnect are two actions, and they sit apart
The record has editable fields now, so it needs a save — and a save is not a reconnect. One writes fields we own; the other leaves for the provider and comes back with a credential we do not own. A connection can be fixed with nothing to save, and settings can be saved while the connection is still dead, so neither can be a mode of the other.
The split is structural rather than a matter of two buttons being differently worded: record actions live in the page header (Save changes, Discard, and the unsaved-changes chip); connection actions live inside the connection card (Check now, Reconnect). Check now moved out of the header in this round for exactly that reason. Every later screen with both kinds of action should place them the same way.
The active switch is a setting, not an immediate action — it waits for Save like every other field. One screen with two save models is how an operator learns to distrust the save button.
The name is ours; the external ID is not
Name is a free field we own and the list pins as its first column. External account is the provider’s and is read-only here. Account ID is the immutable channel_account_id every listing and command binds to. Three names, three owners, and the header says which is which rather than leaving an operator to find out by trying to edit one.
Two axes, and what a broken one does not do
active is the operator’s switch; connection is a system fact. A lost connection does not flip active off — auto-deactivating would silently change what happens on reconnect. The list carries the contradiction instead, and the blast radius here says what it costs: active listings on the account, and jobs blocked on it. Not orders — those are ShipStation and CheckoutChamp’s.
Money rules, and what they close
Margin floor, final value fee and ad rate are the three terms D21’s formula was missing: price − cost − shipping − channel fee − ad fee. MR-064 hard-coded 13% and 2% into a fixture and MR-075 recorded that the floor “has no home”. It has one now. The job still decides whether the ad rate applies (the promoted toggle) — the account only supplies the number.
Ladder models are a shared library, so this is master–detail
It was a dropdown over one editable table, which could not create, rename, duplicate or delete a model — and worse, editing the table silently rewrote whichever model happened to be selected. The section is now a library manager: models on the left with a used-by count, the open model’s tiers on the right.
Every model is library-wide (MR-081). An account picks one; it never owns one. A model therefore carries no owner field — who uses it is derived from the accounts’ own assignment, which is the only way the count stays honest when an account is reassigned. The consequence is that every edit has a blast radius, so the detail pane always says who else moves before you save, and Duplicate is the answer to “I want one number different for one account”.
Opening a model is not assigning it. Clicking a row in the list shows you that model; only Use ‘X’ for this account changes what the account uses. This is the same separation Save and Reconnect have — two gestures that a single control would have quietly merged.
A model in use cannot be deleted, and the button says by whom rather than failing on click.
This settles a conflict rather than creating one. MR-076 says a pack size and its discount tier are one record, so the model owns sizes as well as tiers — which makes MR-012’s “account package set” simply the sizes of the model the account uses. MR-075’s label CA-01 default was a per-account artifact and is gone; the library model is called Standard.
Gross margin is not the margin floor, and must not look like it
The grid carries Pack price, Per unit and Gross margin as separate columns, driven by a one-bottle price and a cost per bottle. Gross margin is (per unit − cost) / per unit — COGS only.
The 35% margin floor sitting a few inches above it is a different measure. D21’s floor is net: price − cost − shipping − channel fee − ad fee. Two percentages on one screen that are not the same measure is a real misread risk, so the hint under the table says which is which, and the column takes no colour for being low. Tinting it under 35% would have invited exactly the comparison the hint warns off; only a negative margin is coloured, because that is unambiguous under any measure.
The price and cost fields are typed or picked: a caret opens a menu of presets, and the field itself takes any number.
A native <datalist> was built first and had to be replaced — it is the wrong primitive. Browsers filter a datalist by what is already in the field, so a box holding 29.95 prefix-matches none of 19.99 / 24.99 / 29.99 and the menu opens empty; the cost box holding 4.50 offered exactly one option, the value already in it. Datalist is a filtering autocomplete, not a preset picker. A preset list has to ignore the typed value, which means owning the menu — about thirty lines, still no dependency, and keyboard-reachable (↓ opens, arrows move, Enter picks, Escape closes).
The example price is a preview, and it found a bug
A discount column of bare percentages does not tell you what the ladder does. The grid now carries an example price computed live from a familiar product and its one-bottle price, using MR-075’s cascade: × 1 = base · × N = base × N × (1 − tier N), with the per-unit figure underneath.
It is a preview and is stored nowhere. MR-075 rung 4 is where a one-bottle price is actually typed, and that belongs to the job. So this control redraws the ladder and is pointedly left out of the dirty check — saving a record must never mean saving a number someone typed to see what a discount does.
Computing it instead of asserting it exposed a conflict in the canonical fixture. SAMPLE-DATA states the MEMP 2-pack ladder seed as $56.98, but 2 × 29.95 − 5% is $56.90 — 8¢ apart. Either the seed is an arithmetic slip, or prices are charm-rounded to .98, which would be a real pricing rule that no decision, spec or screen states anywhere. The screen shows the raw figure and does not round. Flagged, not guessed.
Seeds are cached, not fetched
Shipping and return policies are referenced, never created per listing (MR-025), and the reference list is held on our side per account. eBay business policies change rarely, so a disconnected account stays fully readable and editable — the connection gates the Refresh, nothing else. A periodic re-sync compares ours against the provider’s and disagreement is drift → correct or accept, the pattern SAMPLE-DATA already exercises, rather than a state invented here.
A pack size and its discount tier are one record (MR-076), so adding a size asks for both. Editing a seed never reaches an existing listing or a running job.
Decision trail
MR-025 — operating defaults on the channel account. This is their editor; nothing wrote them before.
MR-026 / MR-012 / MR-076 — the ladder, the package set, and the one-record rule.
MR-064 / D21 — the margin basis. Its three missing terms live here.
MR-066 — the machine-item card, reused rather than redrawn.
MR-070 — create returns and the slow half follows.
MR-075 · reversed — the floor was to be one channel-level number every account inherits. It is an account field instead.
OPEN-ITEMS P1 · closed — the floor has an owner and an editor.
Open — needs Paul
1 · Three eBay accounts can hold three different floors. That is what dropping the channel record costs, and nothing on this screen notices the drift. Taken knowingly; if it bites, the fix is a channel record added later with accounts inheriting, not a rule bolted on here.
2 · The expiry threshold is a guess.expiring lights at 14 days. eBay user tokens run about 18 months, so 14 days is roughly “you see this twice before it matters” — but nothing justifies the number, and it is the only figure on the screen that nothing does.
3 · Should the preview pull the real cost? The cost field defaults to $4.50 as asked, but SAMPLE-DATA holds Memopryl’s actual cost/unit as $3.85, owned by QuickBooks and read-only everywhere else. Choosing a product sets the price and leaves cost alone, which is deliberate for a preview — but if the point is to see a real margin, the real cost is the one to show, and then the field is a read-only fact rather than a knob.
4 · Is $56.98 a rounding rule or a typo? The example column computes $56.90 where SAMPLE-DATA asserts $56.98 for the same 2-pack. If prices are charm-rounded to .98 that rule belongs on the sales channel beside the margin floor, and it changes every price the listing builder derives. If it is a slip, the fixture needs correcting. Nothing anywhere states which.
5 · Where does the model library really live? MR-077 deleted package-builder.html because “two screens that can each define a job’s packages is how they drift”, and Settings › Package sets is a named, still-stubbed nav item. The manager is built here because this is where the ladder is used, but it should be one editor with two doorways — if Settings grows its own, that is the drift MR-077 punished.
6 · Shopify has no seeds block at all. SAMPLE-DATA says CA-01 has no defaults yet, so a Shopify account draws an empty state instead. Whether a storefront genuinely has nothing to seed — or whether its variant structure and price rules belong here too — is a domain question this screen cannot answer alone.