v026 | 2026-09-05 | 508 lines
APPLICATION.md
What the product does
Audience: Claude Code, for any task on the application.
Scope of this file. What the application does and what it forbids — and, at §10, how much of it is built right now.
Out of scope. Not the website, which is WEBSITE.md. And not the consultation services: the application is the analytics half of what HappyShip sells (SCOPE.md). Consultation happens outside it, and the application's only connection to it is the CTA at §9.1.
Read SCOPE.md first. Where the two conflict, SCOPE.md governs — raise it, amend the document, then build.
What else this work needs
| If the task touches | Read |
|---|---|
| How it is built — stack, schema, hosting, build order, auth | ARCHITECTURE.md |
| Screens, flow, what a user does and sees | UX.md |
| Rating, calculation order, accessorials, the rate template | RULES-<leg>.md for that leg |
| What HappyShip charges its own clients, and how it is computed | RULES-HS-CHARGES.md |
The shared documents — visual, dictionary, backlog, conventions — are listed in SCOPE.md under Which document to open, and are deliberately not repeated here.
1. Product and services
1.1 Analytical services
The four are defined in SCOPE.md. Their boundaries are §9 below; which are built is §10.
1.2 Service legs
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.
The strategic goal is rate management across every logistics service; last mile is the near-term focus.
1.3 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 |
1.4 Two kinds of rate card, two kinds of shipment set
Rates.
- Library cards — stored, owned by an entity. Each carries an internally generated rateID, conditions, and a lifecycle (§4).
- Comparison uploads — a file uploaded to run one comparison. Transient, purged after the run (§6); no rateID, no nickname, no lifecycle.
Shipments follow the same split, but only the transient half is built.
- 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 exist to serve invoice audit, which is not built — named here so the symmetry is on record, and not built, stubbed or scaffolded for (§10.2).
1.5 What a phase is
A phase is a decision, not a document. Phases are not folders and not files, and a phase is tied to a leg. Its boundary is described in §10 of this file and nowhere else.
Sections in UX.md and RULES-<leg>.md carry a phase marker in the heading — [phase 1], [not admitted] — so the boundary is visible where it is read. A marker is a label; §10 is the authority. Where the two disagree, §10 governs and the marker is a defect to fix.
Work surfaced during a build is added as a section in the respective document, not as a new file.
Later phases are decided by the product owner, one at a time. Nothing here is a roadmap; the next phase is written into §10.1 when it is chosen.
2. Foundations
2.1 Language in the application
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.
The browser is the default on first arrival for both; the login's settings govern once one exists. These are not two rules — the first is what a login starts as, the second is what it is thereafter. A user may toggle either at any time, and the toggle writes to the login.
EN and CN in phase 1. EN is the source of truth and the user is told so. Domain terms follow DICTIONARY.md.
No hardcoded user-facing strings. All copy routes through the i18n layer from the first screen. One file per locale, identical keys; adding a locale is adding a file, never touching a screen.
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, in CSV or XLSX, is the supported path and is the default. Files that resemble the template but do not conform are rejected with a clear reason rather than interpreted. Support for other formats is decided per client, not assumed.
The same template and the same parser serve library uploads and comparison uploads alike. One ingest path, two destinations.
Non-template files are a service, not a code path. HappyShip staff may convert a client's non-standard file into the HS template for that client's approval. This is a value-added service performed offline, and the converted file then enters through the ordinary path of §4.5 — HappyShip enters it, it lands as draft, the client approves by publishing. It adds no ingest code and no second parser, and it is recorded here so nobody builds one.
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 (§9.1).
A card upload reports two distinct things, and they do not behave the same way. Rejections stop the upload. Confirmations do not, and mixing them either blocks what should not or lets through what nobody looked at. The confirmation set and the plausibility bound are RULES-LAST-MILE.md. This is an act of card upload, not of a run; the substitution acknowledgment of §9.1 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. Which services a card covers is declared after the file parses, not before, and a service the entity does not claim is treated as not offered by that entity. Services within a carrier group usually share by-zone rates, so one grid avoids asking an entity to enter the same 1,050 cells twice, while a Service column on 150 weight rows would be a permanent cost carried for a rare case. Where two services 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.
3. Entities and access
3.1 Entities
[OPEN] — current design, revisited once UX work begins.
There is one entity type. Buyer and seller are not stored roles — they describe what an entity is doing at a given moment, and an entity commonly does both.
- Acting as buyer: purchases logistics rates or services, holds L1 rates from its sellers, validates invoices received.
- Acting as seller: sells logistics rates or services, publishes L4 rates, issues invoices to its buyers.
Implementation rules:
- 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 (§7).
- An entity holding a card in its own library and an entity uploading a file for one comparison are the same entity doing different things (§1.4).
The entity record holds entityID (§4.1), company name, nickname, country or territory, and the publish authorization flag (§4.4). Company name is free text — it may be anything, including a personal email domain — and HappyShip may correct it later without the entityID changing.
The nickname is a short display form of the entity, capped at 20 characters. It exists because the full company name does not fit every list the entity appears in, and nothing depends on it being unique: it is not the entityID and the system resolves nothing against it. It is not the card nickname of §4.1 — that one is a mask an entity puts on its own card, this one is HappyShip's and sits on the entity, and beyond the word the two are unrelated.
Country or territory, on ISO 3166-1, because separate customs territories are separate rows — Hong Kong, Macao and Taiwan each clear their own shipments and take their own rates, which is why a logistics platform carries them whatever their political status. On a create form the value defaults to the browser's region, which is a starting value the operator corrects rather than a detection, and the screen must not present it as one: a defaulted field that looks authoritative is worse than an empty one.
Then the relationship and commercial fields, all initiated by HappyShip and all optional: contact name, contact phone, contact email, location, HappyShip point of contact, contract expiration date or an explicit "no contract" value, services subscribed, profile status, payment method, currency, internal notes, and a document link.
The record is initiated by HappyShip. Some fields the client may then edit, and some are read-only to the client. The contact fields — contact name, contact phone, contact email and location — are client-editable: a client correcting its own address is the same kind of edit as correcting a phone number. Company name, nickname, and country or territory are read-only to the client and remain HappyShip's to correct, because they identify the entity; location does not. The remaining relationship and commercial fields are read-only to the client as well: HappyShip point of contact, contract expiration date or "no contract", services subscribed, profile status, payment method, currency, internal notes, and the document link.
- "No contract" is an explicit value, not a blank. A blank means nobody filled the field in.
- The value sets for services subscribed and for profile status are not settled. Services subscribed depends on
BL-001inBACKLOG.md. [OPEN] - The document link is a URL HappyShip pastes, pointing at a document held outside the platform. It is internal-only and never rendered on a client-facing screen. A link may rot and nothing detects it — accepted deliberately, to avoid a second storage path, a MIME allowlist, a size limit, and a retention rule the purge rules of §6 do not cover.
- Nothing acts on the contract expiration date. It is read, not enforced.
- Currency is the currency in which the client pays HappyShip, not the currency a rate card is denominated in. Rate cards are USD in phase 1 and currency never reaches comparison, so no conversion rate and no conversion date exist 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). The contact email among the relationship fields is reference information for reaching someone and grants no access.
An entity with no logins is a normal state, not an incomplete one. It is what every entity looks like between creation and the first address being logged (§3.2).
An entity may be deleted only while it has no logins and no cards. Once either exists, there is history to keep.
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 §7 rests on that split.
Access is by invitation only. HappyShip logs an email address against an entity, and that record is the account — nothing is created on first arrival. An address that is not logged cannot log in. A code may be sent for the user's convenience, but it resolves to that address and grants nothing on its own.
Internal access is gated by entity binding. HappyShip is itself an entity. A login registered against HappyShip is internal; a login registered against a client entity is not. The address is never inspected — no domain match, no list of internal addresses, no role field.
Two conditions must both hold: the internal surface was reached (§8.1), and the login is bound to HappyShip. Neither alone is sufficient.
An entity may have several logins, and they all see the same thing. There is no per-login view, no per-login permission, and nothing one login of an entity can do that another cannot. Per-login permissions would be a role system under another name, which §3.1 forbids; if it is ever wanted, that is a scope decision rather than an increment.
What follows from that: any login of an entity can publish its cards, and that act binds the entity (§4.4). A card's nickname is the entity's, not the login's (§4.1). The account balance and coupons are the entity's, shared across its logins (§8.4). Language and time zone are the only things held per login (§2.1).
Recording which login acted is not a permission. Publication and withdrawal record the acting login and a timestamp (§4.4). Every login can still do everything; the record answers who, not who may.
One address, one login, one entity. A person needing both an internal and a client login needs two addresses.
There is no edit on a login's identity. The address is the account, so changing it would move an account to a different person, and rebinding to another entity would move everything that login can see. Correction is delete and recreate.
- Deleting a login ends its session immediately, checked per request rather than at session expiry. Delete is used both for a typo and for 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 to it, and deleting a login sends a notification. Both are sent outside a session — before one exists, or after it has ended — so both are EN, for the reason §2.1 gives for the login code email. A delete-and-recreate correction sends a deletion notice and then a welcome within moments, and the wording of both must be neutral enough to survive that. There is no suppression window.
**The mechanism is ARCHITECTURE.md** — how a session is established, how the entity is bound to it, and what the login flow is. This section states only what must be true of it: the entity comes from the login record, never from a request.
3.3 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. An entity that ships differently this week says so this week.
- They are the only input the pool is filtered on (§9.1). Nothing else narrows which cards a comparison runs against.
- They are the entity's own claim. Nothing verifies them, and a false claim produces a comparison against cards the entity cannot actually use.
3.4 Ship-from locations
An entity may hold ship-from locations, and the client maintains them rather than HappyShip. Each carries address, city, state and ZIP code. ZIP is required. An entity may have several.
- Not required. "No physical location" is a valid state for an entity.
- The entity's own address is not a ship-from location. The two are separate.
- Each location maps to a region (§4.7), and the ZIP is what resolves it. The seller may declare a region on a location and is not required to; HappyShip audits the declarations and may remap. The declaration is a hint, never the answer — a must-met condition tested against a seller's own unverifiable claim about where it ships from is not much of a test, and resolving the region from the ZIP makes the same condition checkable.
- Phase 1 stores address, city, state and ZIP only. The warehouse service leg will add attributes to this object; they are not built now.
4. Rate card properties
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 (§8.2).
4.1 Identifiers
- rateID is unique and generated internally, for a stored card. Users never supply or choose it. It is never reused, with one exception: an ID released by deletion under §4.3 may be reissued, because a deleted card was never used or linked, so it reached no report and nothing can point at its ID, and availability is checked against records that exist. The exception is specific to deletion and does not extend to purge. A comparison upload has none.
- An entity may attach a nickname (mask) to a card — a display alias for its own reference, shared by all of its logins (§3.2). The internal ID remains what the system resolves against. A mask may be duplicated, but among duplicates only one can be active. This is the card's nickname and is the entity's own; the entity record's nickname (§3.1) is a different field, HappyShip's, and the two share nothing but the word.
The ID scheme.
- The safe set is the alphabet: digits
3 4 6 7 9and lettersA C D E F G H J K M N P Q R T U V W X Y, uppercase throughout. It excludes the characters that confuse visually — 0/O, 1/I/L, 2/Z, 5/S, 8/B — and that exclusion is the reason the set is not wider. - entityID: one free letter A–Z, then 3 from the safe set. Length 4. Randomized and collision-checked at issue, generated by HappyShip at entity creation. 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.
- rateID, shipment set ID and charge file ID: the entityID, a hyphen, a type letter —
R,SorC— then 5 from the safe set. Length 11.Cis an invoice — a charge file the client uploads for audit. A type letter is fixed and sits outside the random alphabet, so a character excluded from the safe set for visual confusability remains available as a type letter —Sis in use for that reason.Iis not spent becauseCalready reads as invoice and nothing is gained by using it. Invoice audit is not built (§10.2), soCis reserved rather than in use. - **
HSis reserved as an entityID prefix for HappyShip's own entities**, the last two characters being a country code for a branch office. It collides with nothing, at no cost: positions 2 to 4 are drawn from the safe set,Sis excluded from that set for visual confusability, and so no randomly issued entityID can beginHS. This is the same move the type letter makes — a character excluded from the random alphabet stays available for a structured use. Two things it does not change. HappyShip remains a single entity in phase 1. And internal access is unchanged: a login is bound to the entity it is registered against, and the address is never inspected (§3.2). Reserving a prefix allocates identifiers; it grants nothing. The reservation is forward-only — it governs entities allocated from now on, and HappyShip's incumbent US entity predates it, keeps the randomly issued ID it was given, and does not move. - The type letter is fixed and sits outside the random alphabet, so an ID names what it is without a lookup, and a rateID pasted into a shipment field fails validation rather than a query.
- A suffix is unique within one entity and one type, not globally.
- Stored with the hyphen. Input is uppercased and whitespace-stripped before matching. A stored value not matching the pattern is a defect, not a variant.
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 the entity sees only a count and a reason (§7.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.
RateCondition is evaluated per run against the requesting entity's serviceability facts (§3.3). What a condition can express, and how it is evaluated, is RULES-<leg>.md.
4.3 Lifecycle
Draft, active, expired, archived. An expired card cannot generate a new quotation, but can still compute a comparison or an invoice validation.
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.
- Two conditions must both hold to publish: the entity is authorized, and the owning entity sets the flag. Authorization is an entity-level setting HappyShip controls, and it exists so that entering the pool can be charged for. The owner's consent is necessary and is no longer sufficient on its own. Separately, a published card must declare at least one region (§4.7). That precondition is different in kind — the two conditions are consent, the region declaration is completeness — so the two-condition 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 (§6), so the event has something to attach to, while a comparison run leaves nothing behind.
- Expiry clears the flag without a login, being automatic. The record distinguishes the two.
- Sharing a card by rateID is not built. Publication is the only way a card becomes comparable to others.
- A UI may derive a single status label — Draft, Active, Published, Expired, Archived — from lifecycle plus flag. Derived for display, never stored.
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
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. 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.
4.6 Output is whole
The system can always export a complete rate card as one file — everything from the upload plus everything from the form. Input is split (§2.2); the artifact is not. This is what preserves the single-file property for audit and dispute. It applies to library cards, which must export complete.
4.7 Regions
Regions are master data maintained by HappyShip (§8.2): 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.
A region holds 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.
The list is cross-leg, not one per leg — which is what filing regions under platform vocabulary already means in §8.2, a tier defined by not being scoped to a carrier and a leg.
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.4), and that is deliberate — the declaration is coarse on purpose.
The requesting entity states a set of regions per run, or none (§3.3). 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.
The region list as it stands, and the condition's evaluation, are written in RULES-LAST-MILE.md.
5. Invoice and shipment properties
[tbd] — filled when audit invoice and track shipments are scoped (§9.3, §9.4).
6. 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; result detail is purged once the report is delivered; there is no run history.
What survives a run is a small set of durable records that hold no rates, no results and no shipment data. Two classes:
- The issued-ID ledger. A shipment set ID and a charge file ID appear on the delivered PDF, so neither is ever 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.
- The per-entity counters — rates compared, rates published — which survive a run where results do not.
The PDF report is the record. It is emailed to the requesting entity and BCC'd to HappyShip, so a copy persists in email rather than in the platform. It carries the per-run handles of §7.1, since no run record survives to resolve them against.
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 — see §10.1.
The client-facing wording of this is open, tracked as BL-005 in BACKLOG.md. It constrains website copy as well as application copy.
7. Isolation and visibility
7.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 §8.2.
Everything a client owns is private unless published — rate cards, uploads, comparison results.
Card identities are never revealed in comparison output. Results label each counterparty with an opaque reference code. The standard the card names is identified, being public.
The code is keyed on the run as well as the card, so the same card compared twice produces two unrelated codes. That is what stops two entities discovering, by comparing reports, that they were quoted by the same seller — and it is why the rateID, which is stable and identical for every viewer, is never the label another entity reads.
HappyShip resolves a code by re-deriving it across the pool, not by looking it up. No code is stored, so none needs to be: the codes appear in the PDF, HappyShip holds the BCC'd copy (§6), and a consultation matches the two. The BCC is load-bearing for this and not merely a convenience — without it an entity arrives quoting a code against a run HappyShip has no copy of, and the CTA dead-ends.
What HappyShip cannot do is re-run the comparison. The entity's uploaded rates are gone (§6), so a consultation that means modeling rather than introducing needs the entity to upload again. That is a real limit on what the CTA can promise.
Cards excluded by a must-met condition are not shown at all. A comparison reports how many published cards were ruled out and a brief reason — never a card identity, never an owner, and never a breakdown that isolates one card. A reason names a condition type, never a value (§4.2). A reason specific enough to identify a card is a leak carrying a helpful label.
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.
7.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 (§9.1), 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).
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).
8. Internal
8.1 The internal management tool
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.
It is one of two paths that read across entities — the other is the run-time pool (§9.1), which returns computed output only. This one reads card contents and writes on another entity's behalf, which is why it carries its own access control and logs both.
One surface, three exemptions, and they are not interchangeable. The entity roster is every entity's identifying and status fields, the set a list of entities needs: no addresses, no cards, no rates, no internal notes, no document link. The wider seeding set adds the relationship and commercial fields, login addresses, and card metadata — rateID, nickname, derived status, dates — and still reads no contents. The content path reads another entity's card contents, and is the one the paragraph above describes. What separates them is how much of another entity's record a caller ends up holding, which is why one mechanism cannot serve all three (ARCHITECTURE.md, isolation). All three are settled, the content path together with the cross-entity content write, because both are logged and one subsystem records them. The wider seeding set turned out to be two exemptions rather than one, below.
The wider seeding set is two exemptions, not one. The entity record reads one entity at a time: its identifying fields, the relationship and commercial fields, the per-entity counters, its ship-from locations with the region each declares and the region its ZIP resolves to, and its cards' metadata — rateID, nickname, derived status, effective dates. Never card contents. Login addresses read across every entity: the address, the entity it belongs to, and when it was logged, and nothing else. They are separate because one is scoped to an entity and the other spans all of them, and a single bound covering both would be wider than either needs.
What each may write is bounded by its parameter list, as the roster's create is. The entity record writes company name, nickname, country or territory, the relationship and commercial fields, and publish authorization — whose withdrawal clears the publication flag on every card that entity has published (§4.4). It also remaps a ship-from location's region. Login addresses are created and deleted. No card is created here and no card's contents are altered, which is what keeps both out of the content path.
Neither is audited. The audit attaches to reading another entity's card contents, and neither reads them — card metadata is not contents. Of the two writes that reach another entity's records, withdrawing authorization is recorded: a publication event per card, carrying the action, the cause and the timestamp (§4.4). It does not record who withdrew. The event schema attributes an acting login only where the owner acted, and a withdrawal is HappyShip's, so what is on record is that every card left the pool and why — not which internal login decided it. A region remap leaves no record at all. Neither is audited on the rule above, and both are named here so the gap is a decision on the page rather than something inferred from its absence.
What earns an audit row is content, not access. Reading the entity roster is not logged, and neither is card metadata — rateID, nickname, derived status, dates — because neither is another entity's card. Reading another entity's card contents is logged. So is writing on their behalf: entering a card for a client creates something that entity will see and may publish, which changes what they own, and a record of who did that is worth as much as a record of who read.
A row holds who, when, what, on whom, and which card. Who is the acting login's address, snapshotted rather than held as a foreign key — the same choice a publication event makes, for the same reason (ARCHITECTURE.md, schema decisions): login deletion is a hard delete, and a foreign key would either block it or lose the record. What is the action, a content read or a write on another entity's behalf; on whom is the target entity; which card is the rateID. The row is durable and outlives the card it describes, because deleting a card must not erase the record of who read it — it is not run history and §6's purge does not reach it. No client-facing view of these rows exists in phase 1, and whether a client is ever shown them is a later decision rather than an omission.
8.2 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 (§4.7), the metering unit price (§8.4), 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 |
A reference standard is not master data: it is a card entered through the ordinary card path, kept out of the tiers so nobody builds a second entry route for it.
Which of it earns a maintenance page, and which is code. The test is whether it changes on someone else's schedule — and, where it does, how often. If HappyShip changes it and the change alters engine behavior, it is code. Where FedEx or a government changes it, frequency decides: a weekly fuel table cannot be a deploy, while something amended once a year can be, and is.
Pages: ZIP-to-zone · DAS ZIP lists · the fuel table · region mapping · the metering unit price.
Code: zones 2–8 and weights 1–150, grid shape being a template change · the safe-set alphabet and ID format, since issued IDs would stop validating · the basis vocabulary, each value an engine branch · highest-wins and demand-is-additive, rules rather than values · the three condition types, a fourth silently never firing · the carrier list, a carrier being a complete reference set rather than a row · the accessorial catalog, an open catalog being a door to uncontrolled codes · the charges fuel applies to, a flag on that closed set rather than a list of its own · trigger definitions, each an engine test rather than a value a row could carry · minimum billable weights, which arrive with the trigger that imposes them · the dim divisor default, the fallback the engine applies where a card names none.
A FedEx change to anything on the code list is a deploy, not a data edit — the fuel-bearing list included, which FedEx publishes and may amend. That cost is accepted: for last mile these values were already coded in the template, and keeping them coded is what stops the set drifting.
If adding a row requires writing code before it works, the page is a lie.
Internal staff read the coded rules through a rule report on the internal surface (UX.md) — the accessorial catalog, the charges fuel applies to, the trigger definitions, the minimum billable weights and the dim divisor default, read-only, with nothing on it that edits. It is generated from the constants the engine reads, never transcribed, because a hand-written page describing coded rules is a second master whose failure is silent: the page says 48 inches, the engine says 50, and nothing announces the gap. Staff view these rules; they do not edit them.
Reference standards are defined by HappyShip. There is no limit of one per leg or one per carrier. A rate card names the reference standard that applies to it. FedEx published rates for last mile, or Amazon FBA rates for warehouse, for instance.
To be eligible, a standard must be published by its source. Public is the qualifying test, not a consequence of being chosen: designating a rate as the standard does not make it publishable. A negotiated card or a blended average of what HappyShip has seen cannot be a reference standard, however good a benchmark it would make.
A card in a leg with no public standard has no standard to name, and the missing-data substitution of §9.1 has nothing to substitute for that card. The constraint bites per card, and it is a real one for legs beyond the first, recorded here rather than discovered at build time.
Nothing can be compared until master data exists, so this is not a late feature.
A reference standard is updated by replacing the card: the base grid is annual and accessorials move all year, but the template's per-row dated windows carry within-year history inside the file (RULES-LAST-MILE.md, the charge block), so re-uploading the whole card loses nothing that matters. Published reference-standard cards are therefore mutable where client cards are not — a reference standard is a transcription of a public document, not a negotiated agreement someone stands behind.
A replacement does not reach a report already delivered, and nothing detects it: results are purged and the PDF is the record (§6), so a January report names a standard whose values may have been replaced by March, HappyShip's BCC copy included. This is accepted — the report names the standard as of its run date, HappyShip does not retain what it held, and a run cannot be re-run in any case. It holds because a reference standard is a landmark for rate-for-rate rather than a baseline computed against for money, and it must be revisited if invoice audit is later scoped to compute against one.
8.3 Internal reference views
The rule report (§8.2) 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 — it is generated from the repo and served publicly from a Pages project of its own (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 in phase 1 (§10.2).
8.4 Billing and metering
HappyShip never processes payments between buyers and sellers (SCOPE.md).
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.
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.
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.
**What HappyShip charges and how it is computed is RULES-HS-CHARGES.md.**
9. Product functions
Behavior, outputs, and calculation belong in RULES-<leg>.md. This section states only the boundaries. SCOPE.md names all four functions; §10 says which are built.
9.1 Compare rates
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."
One upload, one run. No appends and no accumulation.
The run-time pool. A comparison runs against every published card, filtered by the requesting entity's serviceability facts (§3.3) 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.
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.
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.
**The output surface is UX.md**, constrained by §7.1.
9.2 Manage rates
Lite — upload, conditions, lifecycle, publication (§4). 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 built. 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]
9.3 Audit invoice
Not built. To be scoped. [OPEN]
9.4 Track shipments
Not built. To be scoped. [OPEN]
10. Phase 1
10.1 Built
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 that builds the cross-leg and cross-service foundations, which is why the table groups them.
Grouped, not sequenced. Build order is ARCHITECTURE.md's build sequence; this table says what, not when.
| Group | Built |
|---|---|
| Foundational — cross-leg, cross-service | Auth and login (ARCHITECTURE.md) |
| EN and CN | |
| Isolation | |
| The rate engine | |
| Invitation and coupon codes | |
| Stripe payment | |
| The metering module | |
| The per-entity counters (§6) | |
| Ship-from locations (§3.4) | |
| Regions (§4.7) | |
| Internal | The internal management tool |
| Master data entry and management | |
| The rule report | |
| Read and download of published card data | |
| Rate card library | |
| Service-specific — leg 1, FedEx | Template ingest |
| Rate upload on the HS template | |
| Rate conditions | |
| Self-serve card upload and publication by the owner | |
| HappyShip entry of a card on an entity's behalf | |
| Rate comparison — rate-for-rate and shipment-cost | |
Comparison dashboard, internal and client-facing (UX.md) | |
| PDF report and email delivery | |
| Retention shaping |
Retention shaping is admitted deliberately, because it would otherwise read as the scaffolding §10.2 forbids. Phase 1 purges.
10.2 Not built
Anything not listed in §10.1 is not in scope. Do not build it, stub it, or scaffold for it. Stop and ask — and only start work on it after it has been added to §10.1.
One narrow exception: a screen may carry a placeholder for work that is not admitted, under all of these conditions. It is screen-only: no column, no field in any payload, nothing stored, nothing read. It is greyed, not selectable and not clickable. It carries no value set — a label, never a list of options, because a list is a decision nobody has made. It is visibly marked as not in scope. And the deferred work it names is recorded — as a BACKLOG.md row, or by being named in the not-built list below — so the placeholder is a reminder of something recorded rather than the only record of it. Either satisfies the condition, because the condition exists for the record and not for where it sits, and the not-built list is the stronger record of the two.
The exception is drawn this tightly because an inert label forgets nothing and enables nothing, while a column, a value set or a live control is the scaffolding the rule exists to prevent. Anything failing any of those conditions is not a placeholder, and the rule applies to it unchanged.
Named explicitly, because each is described somewhere and could be mistaken for admitted: derivation of sell rates from buy rates (§9.2) · invoice audit (§9.3) · shipment tracking (§9.4) · stored shipment sets (§1.4) · card sharing by rateID (§4.4) · the internal control tower (§8.3) · 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.