v050 | 2026-09-10 | 652 lines
APPLICATION.md
What the product does
Build tally
What phase 1 admits, and where each item stands. A subset of leg 1: FedEx Ground and Home Delivery only, US continental 48, zones 2 through 8. Two functions — compare rates, and manage rates (lite). This is also the phase carrying the cross-leg and cross-service foundations, which is why the groups below hold them.
Grouped, not sequenced. Build order is ARCHITECTURE.md's build sequence; this says what and where, never when.
✅ built · 🔲 decided, needs code · ⬜ undecided · ⛔ dropped. A row with no glyph is a task rather than a state — neither code to write nor a decision to take.
Verified at d358c2f, by running the suite and reading the tree: 862 passed, 15 skipped, 0 failed. What is built is the repository; this is a reading of it at one moment and never a second master.
Foundational
| State | Item | Ref |
|---|---|---|
| ✅ | Entity, authorization, ship-from locations | §3.1, §3.3 |
| 🔲 | Auth and login | ARCHITECTURE.md, auth |
| ✅ | Sessions and entity binding | ARCHITECTURE.md, auth |
| 🔲 | Delete the dev-only session route when sign-in lands | ARCHITECTURE.md, build sequence |
| 🔲 | EN and CN | §2.1 |
| 🔲 | The i18n layer | §2.1, BL-035 |
| ✅ | Default language, from the login or the browser | §2.1 |
| 🔲 | Time zone | §2.1 |
| ✅ | The ID scheme | §2.5 |
| ✅ | Isolation | ARCHITECTURE.md, isolation |
| ✅ | Adversarial tests, data and view layer | ARCHITECTURE.md, isolation |
| ✅ | The confirmation step on every status change, both surfaces | §2.4 |
| 🔲 | The audit log on the cross-entity write | ARCHITECTURE.md, isolation |
| 🔲 | Each entity is permitted up to three logins | §3.2 |
| ⬜ | The suite runs on every push | ARCHITECTURE.md, isolation |
| 🔲 | Invitation and coupon codes | §10 |
| 🔲 | Stripe payment | §10 |
| 🔲 | The HappyShip metering module and account balances | §10 |
Internal
| State | Item | Ref |
|---|---|---|
| ✅ | The internal shell — the entity and login rosters, one entity's detail, and the authorization control | §9, §3.2, §3.3 |
| 🔲 | Master data entry and management | §9.1 |
| 🔲 | Manage rates — for published and client rates | §4.5 |
| 🔲 | Internal dashboards | §9 |
| 🔲 | Account management — HappyShip charges and AR | RULES-HS-CHARGES.md |
Leg 1 service
| State | Item | Ref |
|---|---|---|
| ✅ | FedEx Ground & Home Delivery template ingest | RULES-LAST-MILE.md, the rate card template |
| ✅ | Charge codes | RULES-LAST-MILE.md, the charge code |
| 🔲 | The rate engine and rate rules | ARCHITECTURE.md, the rate engine |
| ✅ | — the rate-for-rate engine | RULES-LAST-MILE.md, rate-for-rate comparison |
| 🔲 | — the costing engine | ARCHITECTURE.md, the rate engine |
| 🔲 | — charge code behavior: triggers, minimum billable weight, highest-wins | RULES-LAST-MILE.md, FedEx published values |
| 🔲 | The dated weekly fuel surcharge series | RULES-LAST-MILE.md, reference data this leg needs |
| ⬜ | Shipment-cost calculation order | RULES-LAST-MILE.md, shipment-cost comparison |
| ⬜ | The shipment template | RULES-LAST-MILE.md, the shipment template |
| ⬜ | Insurance bracket structure | RULES-LAST-MILE.md, reference data this leg needs |
| ⛔ | ZIP-to-zone lookup — audit era, not phase 1 | RULES-LAST-MILE.md, reference data this leg needs · 2026-09-08 |
| Verify the published values against the FedEx service guide | BL-028 |
Compare rates
| State | Item | Ref |
|---|---|---|
| 🔲 | Rate comparison — rate-for-rate | §5.2 |
| 🔲 | Rate comparison — shipment-cost | RULES-LAST-MILE.md, shipment-cost comparison |
| 🔲 | Comparison dashboard, internal and external facing | §5.4 |
| 🔲 | — the result renders once and is not reachable again; the buyer downloads or emails it, and is reminded before leaving | §5.4 |
| 🔲 | PDF report and email delivery | §5.4, ARCHITECTURE.md, email carries the deliverable |
| 🔲 | The run record — one per run, HappyShip's: the requesting entity, when it ran, the winning rateID, and whether that winner met every must-met condition. No losing cards, and no buyer-facing run history | §7 |
External, general
| State | Item | Ref |
|---|---|---|
| ✅ | The client shell — the four-function menu and the account KPIs | §2.4, §9 |
| 🔲 | Dashboard | §5.4 |
| 🔲 | Manage entity info — logins, locations | §3.2, §3.3 |
| ✅ | Manage rates — change status | §4.3 |
| 🔲 | Stripe payment | §10 |
| 🔲 | ACH payment | §10 |
Manage rates (lite)
| State | Item | Ref |
|---|---|---|
| 🔲 | Client self-serve card upload | §4.5 |
| ✅ | The client rate library — the dashboard and one card's detail | §4.6 |
| ✅ | Publication by the owner — terms acceptance, publish, withdraw | §4.4 |
| ✅ | HappyShip entry of a card on an entity's behalf | §4.5 |
| ✅ | — upload a carrier-published card | §4.5 |
| 🔲 | — and name it a reference standard: the designation | BL-023 |
| 🔲 | — the card-entry screen is reachable from the portal | BL-034 |
| ✅ | Retention shaping — an owning entity and timestamps on every table | §7 |
| 🔲 | Retention shaping — the config-gated purge | §7 |
| Client-facing wording of the retention rule | BL-005 |
Audience: Claude Code, for any task on the application.
Read SCOPE.md first. Where the two conflict, SCOPE.md governs — raise it, amend the documents, then build.
Scope of this file. What the application builds, and how the user experiences it, at high level. What is built is the repository, never a list here; what survives here is the decision behind it.
Out of scope. The website, and the consulting services.
Related but not elaborated here — architecture, style, calculation rules.
The document is evolving.
- The markers are
SCOPE.md, the terms. What they mean is defined there and not restated here. In this file a present-tense passage under one is a decision taken before the build, not a report on running code, and it may change as the build lands. - Do not reject the build if the chat discussion is not aligned with this file — stop and ask.
- The alternatives considered and the options rejected behind a decision are logged in
DECISIONS.mdwhere they are needed. - Work surfaced during a build is added as a section in the respective document, not as a new file.
1. Product and phases
1.1 Services and service legs
The four services are defined in SCOPE.md. Their boundaries are §4 to §6 below; which are admitted is §11.
The legs are listed in SCOPE.md, which also settles that a carrier is a section inside a leg's rules rather than a leg of its own.
1.2 Phases
The application is built in phases, and a phase is a decision, not a document. It is defined by the product owner and is not always strictly tied to a leg.
Later phases are decided by the product owner, one at a time. Nothing here is a roadmap; the next phase is written into the build tally when it is chosen.
2. Foundations
2.1 Language
SCOPE.md sets the source language for both surfaces and points here for how the application handles it.
Language and time zone are per login, not per entity. Language covers the UI and the PDF report; time zone covers timestamps the interface renders, and a date-only field is a date and does not shift. Two logins in one entity may differ in both.
EN and CN in phase 1. EN is the source of truth and the user is told so. Domain terms follow DICTIONARY.md.
User-facing strings are English and are written to be extracted. The i18n layer does not exist yet. Every string the build renders today sits in the template or the module that renders it, and the modules that carry the most of them say so where they sit. What the layer will do when it arrives is unchanged: one file per locale, identical keys, so adding a locale is adding a file rather than touching a screen. Until then the obligation on a new screen is extractability, not routing — a string is written whole, never assembled from fragments or built by concatenation, because a sentence stitched together at render time cannot be moved into a catalog without being rewritten first.
The browser is the default on first arrival for both; the login's settings govern once one exists. The locale the browser reports resolves to CN where it is any Chinese variant, and to EN otherwise. Traditional resolves to CN. The mapping is rewritten when a locale is added. A user may toggle either at any time, and the toggle writes to the login.
The login code email is always EN. It is sent before a session exists, so there is no login to read a preference from.
Layout consequences of length — button sizing, row wrapping, CJK line height — are brand/STYLE.md.
2.2 Ingest
Template. The HappyShip template, maintained by HappyShip, is the supported path and the default. Which file formats the parser accepts is the build's.
The same template and the same parser serve library uploads and comparison uploads alike. One ingest path, two destinations.
Files that resemble the template but do not conform are rejected with a clear reason rather than interpreted.
Non-template files are a service. HappyShip staff may convert a client's non-standard file into the HS template, which then enters through §4.5. In module 1 the handoff is offline, by email. In module 2 the client attaches a file and shares it with HappyShip, and once HappyShip has converted it the client approves the file. In module 3 a recurring non-HS-template file is parsed automatically. At launch, module 1 is sufficient. It is recorded here so nobody builds a second parser.
[UNBUILT] The card-entry path carries a how-to, on the client path and the internal one alike, covering how to fill the template and what is rejected. The internal path exists and carries none; the client's self-serve path is not built.
Column definitions, and the split between what is uploaded and what is entered on the web form, are RULES-<leg>.md.
There is no post-parse edit screen. An entity cannot enter values for lines the file left missing; missing mandatory data is resolved by substitution at comparison time (§5).
A card upload reports two distinct things, and they do not behave the same way. Rejections stop the upload. Confirmations do not. The confirmation set and the plausibility bound are RULES-<leg>.md. This is an act of card upload, not of a run; the substitution acknowledgment of §5 is a different moment.
One grid serves every service the card declares. Service is a column only in the non-grid charge block, never an axis on the weight-by-zone grid. Where two services within a carrier product group have genuinely different by-zone rates, that is two cards: a card spanning several services is several uploads only when their by-zone rates differ.
2.3 Engineering rules
- The rate engine is a standalone library with no framework dependencies. Testable without a database or an HTTP server.
- Explainability is not optional. Every computed charge carries the rule that produced it. A charge that cannot be explained is not returned as computed.
- Prefer failing loudly over defaulting silently wherever a domain rule is involved.
- A test asserts behavior, never the repository's content. A count of documents, markers or sections pins what happens to exist rather than what the code does, so an ordinary documentation commit turns the suite red and the assertion has to be lowered rather than fixed. Assert against a fixture the test constructs, never against the repository the code happens to be run over.
2.4 Interface patterns
Written once, instantiated everywhere. A new leg reuses these rather than copying them.
Two surfaces: internal, and clients.
Navigation has one root per surface, and neither leads to the other. A client arrives at the service menu; the internal tool is a separate surface reached only by a login bound to HappyShip (§9).
Entity context and entity identification are different, and the shell must not collapse them. The internal tool reads across entities, so it shows which entity is in view — a context that changes while the login does not. A client login binds to one entity and has nothing to switch to, so what its shell shows is identification. Rendering the client's as a picker holding one option offers a choice that does not exist.
Sign-out sits in the shell, and it is the only control that ends a session. A session also ends on its own, on limits ARCHITECTURE.md sets; no screen states them.
Language and time zone are set from the shell, taking effect immediately.
The run pattern is upload → validate → acknowledge → run → dashboard → report. A card upload uses the first two stages and stops at stored: nothing about it runs and it produces no report.
- The acknowledgment carries a count, not a policy. A required checkbox stating a consequence in the abstract is agreed blind; the same checkbox beside the number of lines it applies to is a decision (§5).
- The report is the record, because the run leaves nothing behind (§7). A screen implying a result can be reopened is describing something that does not exist.
Exception management. What stopped the work and what was found and did not stop it are both shown for the client's acknowledgment. Card upload, comparison upload and every master-data upload show both, visually distinct.
- Rejection. The blocking column is exhaustive on the first pass, because a fail-fast parser turns a twenty-error file into twenty round trips.
- Non-blocking findings. An apparent deviation from the standard or the norm is highlighted. The test is whether a substitute can be used, or a condition changed, to proceed.
- Empty state. An empty state that is normal must not be dressed as incomplete — a region added but not yet mapped is empty in a way that does mean something is unfinished. The test is whether the empty state is where the thing is meant to rest or a stop on the way somewhere else.
A status change takes a second step, on both surfaces. One click is too little between an intention and a change of state. The step is an interstitial screen rather than a modal: there is no JavaScript in the application and every screen is a document (ARCHITECTURE.md, stack), so a dialog that cannot open is a control that either does nothing or guards nothing. The screen states the consequence concretely rather than asking whether the reader is sure — an abstract confirmation is agreed blind, in the same way the acknowledgment above is.
Deletion takes the step too, though deletion is not a status change. It is the one act that cannot be undone.
Making a card active and publishing it share one review template and remain two confirmations. Publishing carries the review obligation — the owner reviews the card's content and conditions once more (§4.5) — so its body is the longer of the two; making a card active states what changes and stops there. What must not happen is one screen doing both: an entity's own buy rates are active and must never enter the pool (§4.4), and a shared template is not a shared act.
Loading and long-running work is [OPEN]. Nothing beyond "it may take a few minutes" says how long a run takes, what the screen shows while it runs, or whether a user may navigate away. It waits on the job execution model (ARCHITECTURE.md) and on what session expiry looks like part-way through a run.
2.5 The ID scheme
Three rules, not one. An entityID identifies an entity. A typed ID identifies one record belonging to an entity and embeds the entityID that owns it; those two share an alphabet for their generated characters and share nothing else. A reportID identifies one comparison run and is printed on the report the requesting entity takes away. It is the third form, and it generates no characters at all.
An entityID is four characters — one letter A-Z, then three from the safe set. It is generated by HappyShip at entity creation, randomized and collision-checked at issue, and never supplied by a user. It never changes, including when the entity's name is later corrected. It is entityID rather than clientID because HappyShip is itself an entity, and clientID would misname the identifier on HappyShip's own record.
A typed ID is eleven characters — an entityID, a hyphen, one type letter, then five from the safe set. It is unique within one entity and one type rather than globally. Stored with the hyphen; a stored value not matching the pattern is a defect, not a variant. The format itself is the build's.
Two alphabets, because two different things are being protected.
- The safe set governs the generated characters only — positions 2 to 4 of an entityID, and a typed ID's five-character suffix. It excludes the characters that confuse visually — 0/O, 1/I/L, 2/Z, 5/S, 8/B — because a machine produces those characters and a person reads them back, and that exchange is what the exclusion protects. That is the reason the set is not wider, and the alphabet itself is the build's.
- The type letter is chosen, not generated. It sits in a fixed position with no generated character beside it, so nobody has to tell it apart from one. It takes any letter or digit, and the confusable exclusions do not reach it.
[UNBUILT] A reportID is eighteen characters — the requesting entity's entityID, a hyphen, the run's date as YYYYMMDD, a hyphen, one type letter, and a three-digit counter. ABCD-20260910-R001. The entityID is the requester's, the buyer's; the date is the run's; the letter is R for a rate-for-rate comparison and S for a shipment-cost one (§5.2); and the counter is that entity's count of that type on that date, fixed at three digits. It is printed on the report the requesting entity downloads, emails or views (§5.4). Nothing carries one yet — comparison_runs.run_id is an internal surrogate, and the column that holds a reportID is a later build.
The safe set does not reach a reportID, because nothing in one is drawn. Every character is determined before it is written: the entityID is already issued, the date is the run's, the letter is chosen from two, and the counter counts. Uniqueness falls out of a constraint on entity, date, type and counter rather than from a collision check, so there is no generated suffix for the confusable exclusions to protect — and those exclusions exist for the one exchange this form does not have, a machine drawing characters a person reads back. 0, 1, 2, 5 and 8 appear in a reportID and are meant to.
R and S are reused here for unrelated meanings, and the reuse is deliberate. The type-letter catalog below carries R live for a rateID and S reserved for a statementID, and neither is what the letter means in a reportID. The two forms are unmistakable — a typed ID is eleven characters with one hyphen, a reportID eighteen with two — so no value of either can be read as the other, and claiming two further letters to avoid a collision that cannot occur would spend the catalog for nothing. Neither letter is added to the catalog for this purpose: the catalog governs typed IDs and what may be minted into a table that stores one, and a reportID is neither.
A reportID names the buyer to anyone holding the report, its first four characters being the requesting entity's own entityID. That is accepted rather than overlooked: the report is the entity's own, and what it shares is its own business. It reaches nothing that isolation protects — the entity named is the report's reader, not a card's owner, and no counterparty is identified in output (§8.1).
This is why S is a type letter while staying outside the safe set: the two alphabets answer different questions, and S is chosen rather than generated. It leaves the HS reservation below untouched — that argument turns on position 2 of an entityID, and position 2 is generated.
HS is reserved as an entityID prefix for HappyShip's own entities, the last two characters being a country code for a branch office — HSUS, HSCN, HSHK. It collides with nothing because S is excluded from the safe set, so no generated ID can begin HS. The reservation is implemented in the seed, not in the generator: HappyShip's US entity carries HSUS, reseeded rather than migrated, because there is no production data and the migrations are a construction script the fresh-apply test proves. app_roster_create implements no part of it — it draws position 2 from the safe set and nothing more — so a second HappyShip entity would be given a generated ID like any other, and reserving one for it is a change to that function.
The type letters.
| ID | Letter | Is | State |
|---|---|---|---|
| rateID | R | a rate card | live |
| invoiceID | V | an invoice for audit | reserved |
| trackingID | T | a list of shipment tracking numbers | reserved |
| chargeID | C | HappyShip's invoice to a client, an AR charge | reserved |
| statementID | S | HappyShip's statement to a client, a running prepay balance | reserved |
| (shipment profile) | L | a placeholder — a shipment profile is not stored today and needs no identifier | reserved |
Live means a table stores identifiers of that type and constrains the letter — R alone, in rate_cards, whose CHECK accepts an R and rejects every other letter. Reserved means the letter is claimed and no table will accept one. A reserved letter becomes live when the table that stores it is built, and that table carries its own constraint, as rate_cards already does for R.
A table that stores a typed ID constrains its first four characters to the owning entity. rate_cards carries that CHECK beside the letter's own, and every table that goes live with a letter carries the same pair. The rule is what makes a typed ID's uniqueness checkable by the entity that owns it: isolation hides other entities' rows (ARCHITECTURE.md, isolation), so without the constraint a collision could sit in a row the checking session cannot see, and the check would report clear against an identifier already issued. With it, every identifier that could collide belongs to the owning entity and is already visible. A table built without it does not fail loudly — it produces a check that is silently wrong.
R is minted by app_new_rate_id(); no other letter has a generator. It runs under the caller's own binding rather than as a definer, because the owner-prefix rule above makes the collision check answerable from inside the entity's own visibility. It returns a candidate and does not insert, so the write path carries the retry: rate_cards_pkey is the backstop if two sessions draw the same suffix between the check and the insert. An entityID is generated separately, at entity creation. A reserved letter has no generator and no table, and gains both together.
The reassignment cost no migration, and the next one will not be so cheap. S and C carried placeholder meanings in the schema — a shipment set and a charge file — but nothing was ever issued under either, so freeing them moved no stored value. That is the whole reason this was safe, and it is not a precedent: once a letter is live, the rule below governs instead.
A letter is registered in the type-letter catalog, and a generator reads it before minting. app_new_rate_id() refuses unless R is live there, so the catalog is what a generator is checked against rather than a record kept beside one. Adding a letter is still a schema change; the catalog remains master data with no write path, and nothing consults it at any other moment.
A reserved letter can be added or removed freely, because nothing carries it. A live letter cannot. The type letter is stored inside the identifier rather than beside it, so changing one rewrites the stored value of every row that carries it — and the foreign keys referencing those values declare no ON UPDATE, so they reject the update outright. Removing a live letter is a data migration, not a deletion.
3. Entities and access
3.1 Entities and authorization
There is one entity type. Buyer and seller are not stored roles — they describe what an entity is doing at a moment, and an entity commonly does both.
- No
rolefield on the entity record, and no role enum anywhere in the schema. - No separate buyer and seller records, and no separate rate storage per role.
- No role-based branches in the data layer, and no role-based permission checks. Access is determined by ownership and publication (§8).
HappyShip creates the entity record with its first login; the client completes it. The identifying fields — entityID (§2.5), company name, nickname, country or territory — are HappyShip's to set and correct. The client maintains its own contact details and its ship-from locations (§3.3). The relationship and commercial fields are HappyShip's and read-only to the client. Which fields exist is the build; what follows are the decisions about them.
- Country or territory is on ISO 3166-1, because separate customs territories are separate rows. A create form defaults to the browser's region, which the operator corrects; the screen must not present it as a detection.
- The country is the entity's headquarters, not a shipping location. Ship-from locations and their regions are §3.3, and the two are unrelated.
- [UNBUILT] Phase 1 offers four values: the US, mainland China, Hong Kong, and Other. Hong Kong is separate from mainland China for the reason above — separate customs territory, separate row. Other is not a country: HappyShip does no business outside the three in phase 1, so the value captures that the headquarters is elsewhere and records nothing further. The build ships the full ISO list today, and a test pins both its length and that Hong Kong, Macao and Taiwan render, so narrowing it is a code and test change rather than configuration.
- The value sets for services subscribed and for profile status are not settled. Services subscribed depends on
BL-001inBACKLOG.md. [OPEN] - Currency is what the client pays HappyShip in, not what a card is denominated in. Cards are USD in phase 1, so no conversion exists anywhere in this phase.
The entity record carries no account email. Every address that can sign in is a login record, and that record is the account (§3.2).
An entity with no logins is a normal state, not an incomplete one.
An entity may be deleted only while it has no logins and no cards. Once either exists, there is history to keep.
Who may become a client, and on what terms. A client may be created only to compare rates or to subscribe to another service.
- Compare rates — in phase 1 HappyShip creates the client; a client with a company email address may self-register in a later phase. The service is free, on conditions that are [OPEN].
- Manage rates — the client must be created by HappyShip staff and must agree to the service terms and conditions. Self-service is a later phase. The same holds for audit invoices and track shipments.
3.2 Identity and access
One login, one workspace. A login belongs to one entity. entity_id and login_id are separate columns from the first migration — the entity is the isolation boundary, the login is the identity. Everything in §8 rests on that split.
One address, one login, one entity. A person needing both an internal and a client login needs two addresses.
Access is by invitation only. HappyShip creates an entity with its first login email address, and that record is the account. The client may add further logins. An address that is not logged cannot log in.
An entity may bind several logins, and they all see the same thing. [UNBUILT] Each entity is permitted up to three; no limit is enforced today. No per-login view, no per-login permission, nothing one login can do that another cannot — that would be a role system under another name, which §3.1 forbids. Language and time zone are the only things held per login (§2.1).
Passwordless. A six-digit verification code is emailed to a pre-registered address. No stored passwords and no reset flow. There is no sign-up — the first visit and every later one are the same two screens, an address then a code. Code expiry, resend cooldown and wrong-attempt limits follow industry standards; nothing here needs inventing. An unregistered address gets a page with a link to the website's contact form, and a delivery failure has one recovery path: contact us.
A session survives a browser close and ends on its own, on the limits ARCHITECTURE.md sets. Signing out is the only way a user ends their own. What expiry looks like part-way through a run is [OPEN].
Internal access is entity binding, not a role. HappyShip is itself an entity; a login registered against it is internal. The address is never inspected — no domain match, no list, no role field. Two conditions must both hold: the internal surface was reached (§9), and the login is bound to HappyShip.
Recording which login acted is not a permission. Publication and withdrawal record the acting login and a timestamp (§4.4). The record answers who, not who may.
There is no edit on a login's identity. The address is the account, so correction is delete and recreate.
- Deleting a login ends its session immediately, checked per request rather than at expiry. Delete serves both a typo and someone who has left a company, and only immediate termination serves the second.
- A deleted address is free to be logged again.
- Cards that login published stay published. Publication binds the entity, not the login (§4.4).
Logging an address sends a welcome email; deleting a login sends a notification. Both are sent outside a session, so both are EN. A delete-and-recreate correction sends both within moments, and the wording must survive that. There is no suppression window.
The mechanism is ARCHITECTURE.md — how a session is established and how the entity is bound to it. This section states only what must be true of it: the entity comes from the login record, never from a request.
3.3 Regions and locations
Regions are master data maintained by HappyShip (§9): one list, each row carrying an active flag, and no region nested inside another — a region holds ZIP prefixes, never other regions. HappyShip may add a region and re-map afterward.
[UNBUILT] A US region may hold three-digit ZIP prefixes, single or as a range, and a prefix belongs to one region, so a ZIP resolves to exactly one. Others is where a ZIP matching no prefix lands: it holds no prefixes by definition and cannot be deactivated. No prefix is stored today and nothing resolves a region — a location's region is declared directly.
The list is cross-leg, not one per leg — which is what filing regions under platform vocabulary already means in §9, a tier defined by not being scoped to a carrier and a leg.
An entity may hold several ship-from locations, and the client maintains them. ZIP is required. The entity's own address is not a ship-from location. Holding none is a valid state.
[UNBUILT] Each location maps to a region, and the ZIP is what resolves it. The seller may declare a region and is not required to; HappyShip audits the declarations and may remap. Remapping works today; the resolution the audit compares against does not exist, so a declaration cannot yet disagree with anything. The declaration is a hint, never the answer — a must-met condition tested against a seller's own unverifiable claim is not much of a test, and resolving from the ZIP makes the same condition checkable. The screen must not present a declared region as the answer.
The warehouse service leg will add attributes to this object; they are not admitted — carried as placeholders under §11, deferred as BL-008.
4. Rate cards — manage rates
Lite — upload, conditions, lifecycle, publication, all below. Conditions are evaluated per run, so lite is not "no rules." A client's rates are stored in the HappyShip database once standardized.
Full — derivation of sell rates from buy rates, L1 to L4. Not admitted. Its rules are unwritten: margin structure, rounding, minimums and accessorial carry-through are all undecided, and until they are written no estimate of the work is meaningful. [OPEN]
4.1 Definitions
RateTable is leg-specific. Templates are defined by RULES-<leg>.md.
A card names the reference standard it is compared against, chosen at entry from master data (§9).
A reference standard may be converted from a carrier-published rate card or declared by HappyShip as a template. It may or may not have published rates tied to it; with none tied to it, it is the same as a rate template. A reference standard is not versioned. [OPEN]
Rate classes.
| Level | Meaning |
|---|---|
| L1 | Buy rate, from one seller |
| L2 | Consolidated buy rate |
| L3 | Consolidated sell rate |
| L4 | Sell rate, to one or more buyers |
Two kinds of rate card.
- Library cards — stored, owned by an entity. Each carries an internally generated rateID, conditions, and a lifecycle.
- Comparison uploads — a file uploaded to run one comparison. Transient, purged after the run (§7); no rateID, no nickname, no lifecycle.
Shipment sets follow the same split, but only the transient half is admitted. A comparison shipment set is a file uploaded to run one comparison, purged after the run. A stored shipment set owned by an entity would serve invoice audit, which is not admitted — named so the symmetry is on record, and not built or scaffolded for (§11).
How a card is identified, last mile. Reference standard → rate group → rateID → nickname.
- Reference standard is the carrier's published rates. A carrier publishes official rates once a year and continues to issue edits, conditions, new rates and seasonal charges through it. Example: FedEx Ground & Home Delivery 2026.
- Rate group is assigned by HappyShip when a reference standard is selected. It drops the year and keeps the carrier's main services. Example: FedEx Ground & Home Delivery.
- rateID is a rate with a set of conditions defined on the web form. One of the first selections is the rate group, which is how the two are mapped.
- Nickname is user-defined per rateID and shows on the user's dashboard.
4.2 Conditions
A card may carry conditions, and each is marked must-met or prefer-met by whoever sets it. The distinction decides what a non-qualifying entity sees.
- Must-met — a card whose must-met conditions the requesting entity does not meet is excluded from that run. It is not in the pool and cannot be the recommended card. It may still be surfaced as an alternative — priced, and naming the condition types it fails; where it is not, the entity sees only a count and a reason (§8.1).
- Prefer-met — the card enters the pool and is costed, and the result carries an unmet-condition flag. A seller wanting $10k of monthly spend is compared for an $8k buyer, who sees the rate and sees that a condition is unmet.
Prefer-met is lead generation, and is written down so it is not "fixed." A buyer seeing a rate they do not yet qualify for is exactly what drives the consultation CTA. Filtering those cards out would look like a bug fix and would remove the mechanism.
The flag names the condition type and never its value. "Minimum spend not met," never "minimum monthly spend of $10k not met" — the threshold is a term of an anonymized card. Must-met exclusion reasons read identically, so the two cases cannot be told apart by their wording.
A rate card declares one or more regions, or the value meaning all regions, which is exclusive of any specific region. A published card must declare at least one. The card references a region, not a location (§3.3), and that is deliberate — the declaration is coarse on purpose.
The requesting entity states a set of regions per run, or none (§5.1). A card qualifies where its declared regions intersect the requester's set, or where it declares all regions. Stating more regions widens the pool rather than narrowing it.
RateCondition is evaluated per run against the requesting entity's serviceability facts (§5.1). What a condition can express, and how it is evaluated, is RULES-<leg>.md. The region list as it stands, and the condition's evaluation, are written in RULES-LAST-MILE.md.
Multiple conditions set by the rate seller are an AND: the buyer must meet all of them. Multiple conditions set by the rate buyer are an OR, with AND as the perfect match.
4.3 Lifecycle
The lifecycle states are the build's, and the set is closed. An expired card cannot generate a new quotation, but can still compute a comparison or an invoice validation.
Lifecycle is set by the user, not computed. A card is created as draft and the owner promotes it; the owner archives it when finished with, and un-archives it to take it up again. Expiry is the one exception, being automatic.
Archiving is not final. Un-archiving returns a card to plain active and does nothing else. It does not republish: a card re-entering the pool goes through the ordinary publication act with its own consent (§4.4), for the same reason that restoring an entity's authorization republishes nothing. Archive exists so a card carrying history can be put away, not so it can be put beyond reach.
Un-archiving renames on a collision, and only then. A nickname is unique among an entity's active cards, so a card archived under a name another active card has since taken cannot return under it. The card being un-archived is the one renamed, never the incumbent, which is in use. A card with no nickname never collides, and an un-archive that collides with nothing asks nothing.
A card may be deleted only if it has never been used or linked. Once it has been, it is archived, never deleted. Deletion exists to stop unused drafts accumulating, not to remove history.
4.4 Publication
Public or private. Publication is a flag on an active library card, not a lifecycle state, and the two are orthogonal.
This separation is load-bearing. An entity's own buy rates are active and used for its own comparisons, and must never enter the pool. If activation implied publication, every stored card would be exposed the moment it went live, turning a lifecycle transition into an isolation leak.
- Off — private — by default. A draft cannot be published.
- Three conditions must all hold to publish: the entity is authorized, the owning entity sets the flag, and the owner accepts the terms. Authorization is an entity-level setting HappyShip controls, and it exists so that entering the pool can be charged for. Only the client may authorize publishing: the owner reviews the card's content and conditions once more and clicks I agree to HappyShip's policies before publishing, which is the legal condition of entering the pool. Separately, a published card must declare at least one region (§4.2). That precondition is different in kind — the three above are consent, the region declaration is completeness — so the rule is not exhaustive of what a card needs before it can be published.
- Clearing the flag withdraws the card from the pool and returns it to plain active, still the owner's to use for their own comparisons.
- The flag clears three ways and they are distinct events. The owner withdraws it. The card expires. Or the entity's authorization is removed, which withdraws every card that entity has published. Only the first is a decision by anyone at the entity.
- Restoring authorization does not republish anything. The owner republishes, card by card. Publication is the consent record, and a card re-entering the pool because an invoice was paid would make the payment the consent rather than the person.
- Setting and clearing the flag record the acting login and a timestamp. Any login of the entity may do either (§3.2), and the act binds the entity either way — the record says which login, not which was allowed. This is the one place a login is recorded, because publication is the consent that makes a card comparable to strangers, and "someone at your company did" is an assertion where "this login, at this time" is a record. It is cheap here and nowhere else: library cards persist (§7), so the event has something to attach to, while the one thing a comparison run leaves behind is the run record (§7), which names the entity and no login.
- Expiry clears the flag without a login, being automatic. The record distinguishes the two.
- Sharing a card by rateID is not admitted. Publication is the only way a card becomes comparable to others.
- A single status label may be derived from lifecycle plus flag for display, never stored. The label set is the build's. No control may turn publication on as though it were a status step: an entity's own buy rates are active and must never enter the pool, and that separation must not sit one misclick from going active.
- Publishing must be explained where it is offered, because the word suggests display and that is not what happens: a published card is comparable to other entities, never visible to them.
- "You have not published this" and "your entity cannot publish" must be distinguishable. An unauthorized entity meeting a dead control with no explanation concludes the product is broken. What that state offers is [OPEN].
- The three ways a card leaves the pool must not be presented as one event. Only one is a decision by anyone at the entity.
Card versioning is [OPEN] — how a replacement card supersedes its predecessor, and whether conditions and the publication flag carry forward. Whatever is decided has to hold against the rule above that publication is the consent record.
4.5 Who writes a card
Reference standards, including carrier-published rates, are held by HappyShip and are internal only. Held is custody, not ownership of the rates: the rates are the carrier's, and what HappyShip holds is the row — which entity it belongs to under row-level security, and therefore who may read it. HappyShip does not legitimize a rate or stand behind one (SCOPE.md, what HappyShip does not do).
For a client's rate card there are two paths. The owning entity uploads it, or HappyShip enters it on the entity's behalf — including a file HappyShip converted under §2.2. A card HappyShip enters lands as draft, and the owner is emailed when it lands, so a transcription can be checked before anyone stands behind it. Only the owning entity publishes, regardless of who entered the data, and that act is the consent record. HappyShip cannot publish on an entity's behalf.
The internal path diverges from the client's at two points. At the start it asks what kind of card — client, or carrier published — and names the entity the card belongs to; a client has neither, the entity coming from the session. At the end a client's card lands as a draft in that client's library, while a carrier-published card becomes a HappyShip-owned card plus a designation.
The order of what is asked is the same on both paths, and it is this:
- What kind of card — client, or carrier published. Internal only.
- Rate owner — the entity the card belongs to. Internal only.
- The reference standard, from those already designated. It names the carrier, the service group and the leg, so choosing it settles all three and they are read rather than asked for. A carrier-published card branches here: it has no standard to name because it becomes one, so it asks for carrier and service group directly, those being what determine the template's shape.
- Describe the card — nickname, dim factor, FSC discount, currency and weight unit, notes.
- Thresholds — the conditions, each marked must-met or prefer-met with no default (§4.2).
- Download and fill.
- Upload, then declare which services the card covers, after the parse.
- Saved as draft.
The card's details are settled before the file is downloaded, and the effective dates sit in the file's header block rather than on the form. No reason is recorded for the ordering beyond that it was chosen during design (DECISIONS.md).
4.6 The library dashboard
The output of manage rates is the user's dashboard, showing the state of the rate cards the entity maintains.
5. Comparing — compare rates
5.1 Serviceability facts
What the requesting entity tells the system about itself for one run — where it ships from and to, what volume, what service level, and whatever else a leg's conditions test. The full set per leg is RULES-<leg>.md.
Three properties matter and are easy to lose:
- Supplied per run, through the web form. Not stored on the entity, not remembered between runs.
- They are the only input the pool is filtered on.
- They are the entity's own claim. Nothing verifies them, and a false claim produces a comparison against cards the entity cannot actually use.
5.2 The two comparisons
- Rate-for-rate — when two cards have exactly the same structure, the numerical differences can be normalized and compared. The result is "who is cheaper for each line item."
- Shipment-cost — when an actual shipment profile is presented, each card produces the cost for that shipment set, whether or not the two cards share a structure. The result is "shipping cost."
The two are chosen first, because they take different inputs. Serviceability facts are then supplied on the web form (§5.1), and the rates or shipments either uploaded on the HS template or — for rate-for-rate — selected from the entity's own library. An uploaded file here is transient and nothing may offer to keep it.
One upload, one run. No appends and no accumulation.
Comparing against your own card. An entity using manage rates may run a comparison against one of its own stored cards, by reference, and against the standard that card names. A comparison never promotes a comparison upload into a stored card.
5.3 The run-time pool
A comparison runs against every published card, filtered by the requesting entity's serviceability facts (§5.1) against must-met conditions (§4.2). The pool is computed fresh for each run and does not persist. No stored relationship governs who compares against whom, and a newly published card is comparable immediately; there is no review step.
Missing mandatory data. Where an uploaded card lacks mandatory data for a line, the standard the card names is substituted for that line; the line is not excluded. The entity acknowledges this at upload through a required checkbox, shown alongside the count of affected lines. Every substituted line is marked in the report. Results also report the count of the entity's own priced lines that exceed the standard the card names. The alternative considered was excluding the line from both sides. Substitution is the product owner's decision.
5.4 Output
The output is a dashboard result, with the option to download an executive summary. It renders once and is not reachable again, and nothing stores it for retrieval. This is acceptable rather than hostile because nothing is destroyed: a buyer who loses the result re-uploads and runs again, and is answered against the pool as it stands that day. The reason is written down because renders-once reads as an oversight without it, and the repair a later reader reaches for is persistence. The output does not list rateID, entityID or any other identifier — where a client is happy with a rate, the next step is to contact HappyShip.
What the dashboard and the report must both carry, constrained by §8.1. Layout is [OPEN].
- An executive summary — where the entity's card stands in the pool, against the reference standard and against the recommended card; and where a shipment profile was supplied, the total cost difference against both.
- Counterparties unlabeled — a row carries its figures and nothing that names the card or persists across runs (§8.1). The reference standard is named outright, being public.
- Marked substitutions — every line priced at the reference standard because the card lacked mandatory data.
- The count of the entity's own priced lines above the reference standard.
- The recommended card — the cheapest published card meeting every must-met condition, where it beats the entity's own. How cheapest is determined is
RULES-<leg>.md. - Alternatives — published cards cheaper than the recommended, each blocked by one or more must-met conditions, priced individually and ordered cheapest first. Each names the condition types it fails and nothing else about itself (§8.1).
- Excluded cards as a count and a reason, naming the condition type only. This covers every published card ruled out and not surfaced as an alternative.
- Prefer-met flags, with the rate and the condition type.
- The measure's caveat — rate-for-rate ranks on an average, across a weight band and across zones. Every rate-for-rate output carries this and points to shipment-cost (§5.2) as the more accurate answer. It qualifies the recommended, every alternative and the competitive result alike rather than sitting on one of them, and it belongs to rate-for-rate only: shipment-cost averages nothing.
- The competitive result — where there is neither a recommended card nor any alternative, the output states that the entity's rate is competitive and offers to publish it. That offer is the call to action in that case; the consultation CTA below is the one everywhere else.
- The consultation CTA.
After the run the screen must be honest that the result renders once rather than implying it can be reopened. What it offers is the download and the option to email it. The buyer is reminded of that before leaving the view, so that a buyer who wanted a copy is not told after the fact. Neither artifact is required and taking neither is a choice the screen allows.
6. Audit invoices and track shipments
[OPEN] — filled when the two are scoped. Neither is admitted (§11).
7. Retention
Library cards are stored and persist (§4).
Comparison uploads, computed results and run detail are not. The uploaded file is purged once parsing succeeds, and result detail is purged once the run ends. The buyer has no run history and no result is reachable a second time (§5.4). HappyShip keeps a run record, the third durable class below. The two are a distinction rather than a caveat on one another: what does not exist is a buyer-facing history, and what does exist holds no rates, no results and no losing cards.
What survives a run is a small set of durable records that hold no rates, no results and no shipment data. Three classes:
- The issued-ID ledger. An ID that appears on a delivered PDF is never reissued, even after the file it named is purged. The ledger holds an ID and the date it was issued. It is not run history and does not make a purged run retrievable. [UNBUILT] Which ID types the ledger covers follows the revised scheme at §2.5. No ledger exists:
issued_idswas dropped in migration 0015, and nothing had ever been issued under it, so a ledger is new work rather than a migration of stored identifiers. - The per-entity counters — rates compared, rates published — which survive a run where results do not.
- The run record. [UNBUILT] One per run, holding the requesting entity, when the run happened, the winning rateID, and whether that winner satisfied every must-met condition. It does not hold the losing cards. Storing them would accumulate a record of which sellers were undercut by which others, which is the accumulation isolation exists to prevent, and nothing in the product wants them. It is HappyShip's record, not the buyer's, and it is what resolves a later consultation back to a run (§8.1). The same comparison run twice produces two records, correctly — the pool moves between runs as cards are published and cards expire, so the same upload on a different day is a genuinely different answer. A run record is a snapshot claim against the pool as it stood, never a standing fact.
What the buyer keeps is whatever artifact it chose to take. The result renders once (§5.4); the buyer may download it, email it to itself, both or neither. Email is not mandatory and there is no BCC to HappyShip. What resolves a later consultation is the run record above, not a copy of the report.
Purge is configuration, not a hardcoded code path, and defaults to on. The tables carry an owning entity and timestamps from the first migration, so a later phase can enable retention without a rewrite.
The client-facing wording of this is open, tracked as BL-005 in BACKLOG.md. It constrains website copy as well as application copy.
8. Isolation and visibility
8.1 Visibility classes
Reference standards are public, and clients are not blinded to them. A client's rates and shipments are always compared against the standard its card names, and that standard is named in output. What qualifies as a reference standard is §9.
Everything a client owns is private unless published — rate cards, uploads, comparison results.
Card identities are never revealed in comparison output. A counterparty is unlabeled: a row carries its figures and nothing that names the card, nothing that identifies its owner, and no handle that holds from one run to the next. The standard the card names is identified, being public. The requesting entity's own card is the one exception: it stays in the pool like any other published card, and it is labeled as the entity's own. Anonymity buys the entity nothing against itself, and an entity holding two published cards would otherwise meet one of them as a competitor it could not tell from a stranger's. The rule stands unchanged for every other card in the run.
No identifier of a counterparty reaches output at all. The rateID never could: it is stable, identical for every viewer, and its first four characters are the owning entityID (§2.5), so it names the owner to anyone who has read the ID scheme. Nothing stands in for it either — a per-run substitute was the previous answer and is superseded (DECISIONS.md). What stops two entities discovering, by comparing reports, that they were quoted by the same seller is that neither report carries anything to match on.
HappyShip resolves a consultation through the run record (§7), which names the requesting entity, when the run happened and the winning rateID. An entity arriving to ask about the rate it was shown is answered from that record.
What HappyShip cannot do is re-run the comparison. The entity's uploaded rates are gone (§7), so a consultation that means modeling rather than introducing needs the entity to upload again. What it can do without a re-run is name the winner, which the run record holds. The limit on what the CTA can promise is real and is narrower than it was: introducing the winning seller needs no upload, and modeling anything still does.
Rate-for-rate output has three row shapes, and what a row may carry depends on which one it is. A single rule for all three would be either too tight for the anchor or too loose for an alternative, so the bound is stated per shape.
| Shape | What it is | What the row carries |
|---|---|---|
| anchor | The entity's own card, C | The matrix |
| recommended | The cheapest published card the entity qualifies for, where it beats C | The matrix against C, and any prefer-met flags |
| alternative | A published card cheaper than the recommended, blocked by one or more must-met conditions | The ranking figure and the condition types it fails |
An alternative carries those two things and nothing else. The fields are rank_value and unmet_condition_types; they are named here so the bound can be checked against the build rather than only agreed with. run_ref was a third and is gone with the reference code it carried. What a matrix would add is the card's line detail, which is why the shape has none.
A card excluded by a must-met condition may be priced and shown as an alternative (§5.4). It appears as a price and the condition types it fails, unlabeled like every other counterparty — never a card identity, never an owner, never a threshold value, never line detail and never a matrix. A reason names a condition type, never a value (§4.2).
The count-and-reason rule still governs every published card not surfaced as an alternative. A comparison reports how many were ruled out and a brief reason, and for those cards that remains the whole of what a run produces.
This reverses a rule, and the sentence that justified it no longer decides the case. The rule was that a breakdown isolating an excluded card is forbidden, because a reason specific enough to identify a card is a leak carrying a helpful label. That claim is about a reason, and it still holds of one: a reason narrow enough to pick out a single card would name a value or an owner, which is why neither may be shown. An alternative does not carry such a reason. The condition types it fails are the requesting entity's own serviceability facts read back to it (§5.1), so every entity failing the same condition sees the same words, and the words identify the entity's position rather than the card.
What is conceded is the price, one card at a time rather than in aggregate. That is a real widening of what a non-qualifying entity learns, and it is taken deliberately: an entity cannot know in advance which of its own must-met failures is worth relaxing, so the lever and the prize have to appear together or neither is actionable. The trade and the option rejected are DECISIONS.md.
Must-met still decides whether a card can be recommended, and it no longer keeps that card's price from an entity that does not qualify. What it never did was hide the card's existence — the count already reported that.
None of this is seller-facing. A seller learns nothing about who was shown its card, or how often, and no output runs in that direction.
Publishing a card is a deliberate act by its owner (§4.4), and it is what makes the card comparable to other entities. It does not reveal the card, the owner's identity, or any relationship between the two. Only computed results reach another entity.
8.2 Enforcement
Isolation is enforced at the data layer, not in application code, so a missing filter fails closed rather than leaking. The entity comes from the session, never from a request (§3.2). The mechanisms are ARCHITECTURE.md.
Any path that reads across entities requires an explicit test asserting that entity A cannot read entity B's resources. It requires the positive assertion beside it — that the permitted party can — and a third asserting the bound itself, so widening the reachable column set fails a test rather than passing silently. Both directions or neither counts: a suite asserting only the negative passes when the exemption is broken, missing, or was never built, because the negative is satisfied by the feature not existing, and only the positive tells a correctly bounded exemption from one that has silently failed closed. Those tests extend to the run-time pool (§5.3), where two distinct layers are asserted separately, because they test different mechanisms and merging them hides which one failed:
- Publication state — whether the flag is set at all. A comparison must not surface an unpublished card, a draft, an expired card, or a card whose entity's authorization was removed (§4.4).
- Per-run filtering — of cards legitimately in the pool, excluded for this requester. A card whose must-met condition the requesting entity does not meet must not surface (§4.2).
[UNBUILT] The content path asserts five things. Positive: an internal session reads another entity's card contents. Negative: a client session cannot, by any route. The bound: the reachable column set is named, so widening it fails a test rather than passing silently. The audit: the action writes its row, a failed audit write fails the action rather than letting it proceed unlogged, the row cannot be altered or deleted through the path, and the application role cannot read the table at all. The boundary: the roster path cannot reach card contents, because two exemptions with no test between them become one mechanism sooner or later.
Neither layer returns card content as opposed to computed output.
A third assertion covers the act rather than a run: removing an entity's authorization clears the publication flag on every card that entity has published, not merely on some (§4.4).
9. The internal surface
A separate, HappyShip-only surface. Not a client feature and never exposed to clients. Access is §3.2: the internal surface, plus a login bound to the HappyShip entity.
The client surface opens on a menu of the four functions, labeled exactly as SCOPE.md names them — one vocabulary across the documents and the interface. A function not admitted is inert: a label, not a route, a stub, or a screen that explains itself.
The internal tool covers five areas:
- Manage clients — create, delete and archive an entity, authorize an entity to publish, set the document link. The terms on which a client is created are §3.1. A client is responsible for its own locations; mapping them to regions is optional for the client and may be overwritten by HappyShip staff.
- Manage master data — countries and territories, regions, rate groups, the published national fuel surcharge index.
- Manage rates — create reference standards, define HappyShip charge codes, publish carriers' published rates, draft a client's rates.
- Manage billing — view a client's account, record ACH payments manually, issue coupons.
- Reference documents — rules, vocabulary.
9.1 Master data and the reference standard
Master data has three tiers, cut by whether a carrier owns it — the cut that decides what adding a second carrier costs.
| Tier | Holds | A new carrier |
|---|---|---|
| Platform vocabulary | Owned by nobody: countries, currencies, regions (§3.3), the metering unit price (§10), the value sets left open | changes none of it |
| Carrier reference | Scoped to a carrier and a leg: zone tables, DAS lists | means one complete new set |
| Dated series | Fuel only today. Carrier reference with a time axis, separate because every read needs a governing date | as carrier reference |
9.2 Internal reference views
The rule report is the internal reference view, and its master is a code module rather than a governance file — which is why it serves the application alone rather than both surfaces, and SCOPE.md does not carry it, under the anti-drift rule every view here holds to: regenerated from its master, never edited in place, or the page becomes a second one. The handbook was the other one and is no longer on this surface at all (SCOPE.md), so it is neither internal nor hosted by the application, and it is looked for there rather than here.
An internal control tower for auditing work performance is intended and is not admitted (§11).
10. Billing and metering
HappyShip never processes payments between buyers and sellers (SCOPE.md).
[UNBUILT] Billing covers HappyShip's own clients for HappyShip's services. Stripe is the payment path. ACH and wire are expected later and are managed manually. An entity sees its own account balance; the internal team sees every client's.
[UNBUILT] Coupon codes are stored value in USD. Single-use, expiring three months from issue. An entity may hold several; they draw down first-expiring-first. Invitation and coupon codes are single-purpose and independently redeemable. Invitation is §3.2.
[UNBUILT] Metering is per run. The unit price is master data set by HappyShip, defaulting to $0 in phase 1. The deduction path is real and runs on every job regardless of price, so the block-at-exhausted-balance state is exercised rather than stubbed. What is unsettled is the price, not the mechanism — plan structure, pricing and entitlements are BL-001 in BACKLOG.md.
[UNBUILT] The account surface shows the balance and each coupon held with its expiry, rather than one merged number. Stripe is the path; ACH and wire are instructions rather than a form that submits. An exhausted balance blocks a run and leaves the account reachable so the entity can pay — what that blocked state looks like at the point of running is [OPEN].
What HappyShip charges and how it is computed is RULES-HS-CHARGES.md.
11. Not admitted
The boundary. What phase 1 admits, and where each item stands, is the build tally.
Anything not in the build tally is not in scope. If you are asked to build it, stop and ask — and only start work on it after it has been added to the tally.
A dropped item stays in the tally as a ⛔ row with the date it was dropped, never deleted, so this rule keeps a stable referent and nobody has to reconstruct why something is out.
A placeholder is allowed, to remind us of a future build. It must be greyed and not clickable, it carries no value set, and it stores nothing. The deferred work it names must be recorded — as a BACKLOG.md row, or by being named in the not-admitted list below — so the placeholder is a reminder of something recorded rather than the only record of it.
Retention shaping is admitted deliberately, because it would otherwise read as the scaffolding this section governs. Phase 1 purges.
Named explicitly, because each is described somewhere and could be mistaken for admitted: derivation of sell rates from buy rates (§4) · invoice audit (§6) · shipment tracking (§6) · stored shipment sets (§4.1) · card sharing by rateID (§4.4) · the internal control tower (§9.2), which the run record (§7) does not admit — a record is data and the tower is a view over data, and the view is built when it enters the build tally and not before · any leg but the first and any carrier but FedEx (SCOPE.md).
This boundary is the application's alone. The website has its own at WEBSITE.md; neither admits nor forbids anything on the other.