v027 | 2026-09-07 | 529 lines
APPLICATION.md
What the product does
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.
- UX decisions made pre-build are marked [UNBUILT]. They may change during the build, and a present-tense passage is not a report on running code. The marker closes when the thing is built.
- 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. - Open items are marked [OPEN] or recorded in
BACKLOG.md. - 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 §11.1 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.
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 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.
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.
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.
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.
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
The safe set excludes the characters that confuse visually — 0/O, 1/I/L, 2/Z, 5/S, 8/B. That exclusion is the reason the set is not wider, and the alphabet itself is the build's.
An entityID 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.
**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 forward-only: it governs entities allocated from now on, and HappyShip's incumbent US entity predates it, keeps the ID it was given, and does not move.
A typed ID is the entityID, a hyphen, a type letter, then a suffix, 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.
[UNBUILT] The type letters are being reassigned, and two of them already mean something else. The build issues R, S and C, where S is a shipment set and C a client charge file. The scheme below reassigns both and adds two more, so it is a change to live identifiers rather than an extension.
| ID | Type letter | Is |
|---|---|---|
| rateID | R | a rate card |
| invoiceID | V | an invoice for audit |
| trackingID | T | a list of shipment tracking numbers |
| chargeID | C | HappyShip's invoice to a client, an AR charge |
| statementID | S | HappyShip's statement to a client, a running prepay balance |
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.2, 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.2).
How a card is identified, last mile. *[→ RULES-LAST-MILE.md; stays here until that file receives it.]* 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 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.
*[→ RULES-<leg>.md; stays here until that file receives it.] 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.* [move to RULES?]
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. Expiry is the one exception, being automatic.
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. [UNBUILT] — the third is new; the build enforces the first two. 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 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 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 HappyShip's and internal only.
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. Neither the dashboard nor the downloaded report is stored for retrieval. 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 cheapest serviceable rate; and where a shipment profile was supplied, the total cost difference against both.
- Counterparties labeled by a reference code, different in every run. The reference standard is named outright, being public.
- The same codes in the PDF, since no run record survives to resolve them against later.
- 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.
- Excluded cards as a count and a reason, naming the condition type only.
- Prefer-met flags, with the rate and the condition type.
- The consultation CTA.
After the run the screen must be honest that nothing is retained rather than implying a result can be reopened. What it offers is the download, and the report already in the entity's inbox.
6. Audit invoices and track shipments
[OPEN] — filled when the two are scoped. Neither is admitted (§11.2).
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; 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. 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. The ledger today accepts a shipment set and a charge file; both letters are reassigned under that scheme, so this cannot be built without migrating what is already issued.
- 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 §8.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.
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. 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 (§7), 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 (§7), 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.
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.2).
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. Phase 1
This section says what phase 1 admits, not what exists. What is built is the repository — the migrations, the modules and the tests — and no list here can stay true to it. A passage describing admitted work that does not exist yet carries [UNBUILT].
11.1 Admitted
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 carries 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.
Retention shaping is admitted deliberately, because it would otherwise read as the scaffolding §11.2 governs. Phase 1 purges.
| Group | Admitted |
|---|---|
| Foundational | Entity, authorization, ship-from locations |
| Auth and login | |
| EN and CN | |
| The ID scheme | |
| Isolation | |
| Invitation and coupon codes | |
| Stripe payment | |
| The HappyShip metering module and account balances | |
| Internal | Master data entry and management |
| Manage rates — for published and client rates | |
| Internal dashboards | |
| Account management — HappyShip charges and AR | |
| External, general | Dashboard |
| Manage entity info — logins, locations | |
| Manage rates — change status | |
| Stripe payment | |
| ACH payment | |
| Leg 1 service | FedEx Ground & Home Delivery template ingest |
| Charge codes | |
| The rate engine and rate rules | |
| Compare rates | Rate comparison — rate-for-rate |
| Rate comparison — shipment-cost | |
| Comparison dashboard, internal and external facing | |
| PDF report and email delivery | |
| Manage rates (lite) | Client self-serve card upload and publication by the owner |
| HappyShip entry of a card on an entity's behalf | |
| Retention shaping |
11.2 Not admitted
Anything not listed in §11.1 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 §11.1.
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.
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) · 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.