Actaurus返回 Actaurus

Case Study · 2026

NextJune

A quote-to-cash workspace for air freight forwarding — where the system carries the memory, and the person makes the call.

Operational state modeling · Decision support · Reconciliation feedback loops

Next.js · TypeScript · Supabase · Cloudflare Workers · Synthetic data throughout


The problem

Pricing a shipment draws on three things, and they live in three different places. One of them is somebody's memory.

A freight forwarder receives an enquiry, asks several carriers what they would charge, picks one, adds a margin, sends a quote, and — weeks later — reconciles against whatever the carrier actually invoiced. To do that well you need three things at once: the prices carriers are quoting today, which arrive by email; what comparable shipments were priced at before, which sits in folders; and what margin this particular client gets, which is usually carried in someone's head.

The consequence is that the work is gated on recall. Experience becomes the bottleneck: a new person cannot quote without it, a stored default silently drifts from what is actually being charged, and mistakes surface long after they were made — a senior colleague catches a junior's error the same week, while the junior catches the senior's a year later, during reconciliation.

NextJune is built on one idea about that: the person makes the decisions; the system remembers, and points at what deserves attention. It never chooses a carrier or sets a margin. It makes sure that whatever somebody would have needed to remember is already on screen at the moment the decision is being made.


The rule I set for scope

Solve the general problem, or say out loud that it is not one.

The obvious failure mode for a tool built out of domain experience is that it encodes one company's habits and calls them an industry. So the brief carried an explicit instruction: build for what small and mid-size forwarders share, and if a requirement turns out to apply to only one operation, say so and stop rather than building it.

That rule is why the data is synthetic end to end, written from scratch rather than derived from anywhere. It is also why the demo below is open: there is nothing in it that needs protecting, so the whole system can be looked at and argued with.


The shape of the work

Quoting is not a form. It is a form with a wait in the middle.

Carriers usually come back within a few hours. That makes the sequence gather → ask → wait → return → price, and it makes the interesting moment the one that is easiest to lose: the enquiry that has not come back. Past half a day, somebody has to notice and chase, and the reason they usually do not is that nothing is keeping track.

A system that models quoting as a single form is modelling a different job. This one models the wait.


Decisions

Each of these exists to take something out of someone's head and put it on the screen.

Modelled

A quote exists before its prices do

You have asked three carriers and none has answered. A spreadsheet records that as nothing at all — the rows simply are not there, which is indistinguishable from never having asked.

Here an enquiry exists from the moment it is sent, with no rate and an explicit status: pending, responded, no response, declined. Four different facts, where a blank cell would have been one. Only some of them mean somebody should chase.

A draft quote showing three carrier inquiries, none answered yet, with rate fields reading 'fill when received'
3 inquiries · 0 responded · 3 pending. The rate fields are not empty, they are awaiting — and the pinned note under the carrier is the kind of thing that normally only exists in somebody's experience.
Computed

Who to ask is worked out, not recalled

Knowing which carriers to approach for a given lane is exactly the knowledge that takes years to accumulate and is never written down. It is also derivable: it is in the history of who was actually booked.

Carriers are suggested per route from that history, scored on how often they were selected, less a penalty when they did not reply or when their invoice later came in disputed. The weights are deliberately crude and not fitted to anything — a dispute costs more than a single silence, but neither can fully cancel out a carrier who has actually been booked on that lane repeatedly. The score is a retrieval aid, not an automated carrier selection model.

One detail only showed up in the data. Matching a trucking lane on both the pickup and delivery postcode almost never matched anything, because the customer side changes every shipment. Only the airport end is a stable anchor. That was a discovery, not a design.

Surfaced

Recent margin outweighs average margin

Every client has a stored default markup. It goes stale quietly: rates move, the person quoting adjusts, and nobody goes back to update the record. The average across all history hides that — the recent quotes are the ones that say what is actually being charged.

So the markup shown is weighted toward the last few quotes, and when it has drifted from the stored default by more than a couple of points, the form says so, with both corrections one click away. It does not pick one.

Quote pricing step showing stored default 15%, recent actual 18.4%, and a warning offering to use 18.4% or update the contact record
Stored default 15%, recent quotes 18.4%. This is the whole thesis in one line: something a colleague would have had to know, said out loud at the moment it matters — and still left as the person's decision.
Connected

Reconciliation is a pricing input, not an accounting chore

The carrier's invoice arrives weeks after the shipment. Reconciling it means comparing against what they quoted — or, when no enquiry was sent because the cargo was ordinary, against the contract rate on file. That is the baseline; enquiries are for the shipments that fall outside it.

Which means a variance is rarely just a billing error. It can be a re-weigh on arrival. It can be a rate card nobody has refreshed, quietly mispricing every quote on that lane until someone notices. Reconciliation is where those surface, so it is wired back into quoting rather than parked in accounting.

Vendor reconciliation panel showing a flagged variance, a warning that the contract rate on file may be out of date, and a button to copy a dispute email
Billed below the contract rate on file — it may be out of date. This shipment came back $601.71 under, because the carrier charged less than the stored rate. Which means the stored rate is wrong, and has been inflating every quote on that lane. The system noticed; the "Update Contract Rate" button is right there. Next to it, the dispute email is already written.
Modelled

Estimated cargo and actual cargo are different facts

Customers quote from estimates. After packing, the weights and dimensions change — not occasionally, but as a matter of course. A system that lets you overwrite the estimate loses the thing reconciliation needs, which is what you were quoting against when you set the price.

So actual cargo is confirmed as a separate step rather than edited over the top, and the quote carries both. Special handling works the same way: hazmat, temperature control, non-stackable and oversize are recorded on the quote because they are what makes a contract rate unreliable and an email enquiry necessary.

A quote flagged non-stackable and temperature controlled, where one airline responded and another declined the temperature requirement
One carrier confirmed 2–8°C, the other declined it. The decline is kept rather than deleted. Next time similar cargo moves on this lane, that is already known.

Deliberately out of scope

The first version of this was ten modules. That version was not finishable, and an unfinishable project is indistinguishable from an abandoned one.

The original sketch covered everything a forwarder does, and was meant to be reusable across industries besides. It was cut, on purpose, to the spine that has a beginning and an end: an enquiry arrives, an invoice goes out. Customer relationships, quoting, invoicing and vendor reconciliation stayed. Document generation, role-based permissions, a service catalogue and a sharing hub did not — those belong to the general platform idea, which is a different product from a freight quoting tool and should be built as one if it is ever built.

Reconciliation and payments remain listed as planned, because they extend this same spine: what a supplier quoted, what they later billed, and the money that actually moved.


How this was built

I defined the operational model, product rules, state transitions, pricing behavior, reconciliation logic, edge cases, and acceptance criteria based on freight-forwarding domain experience. An AI coding agent produced the implementation; I reviewed system behavior, tested edge cases, and directed revisions. All demo data is synthetic and was created specifically for the project.

Where it is now

Deployed on Cloudflare Workers with Supabase behind it. Money arithmetic is rounded to cents at every step so the application and the database never hold different numbers for the same quote, with seventeen tests that import the real module rather than restating it. Lint, types, tests and a build run on every push.

The demo below is a real account with its own seeded history — invented clients, invented carriers, invented shipments — isolated by the same row-level security that separates any two users, and rebuilt nightly. You can change anything in it.

Case Study · 2026

NextJune

一个面向空运货代的 Quote-to-Cash 工作台——由系统承担记忆,由人做决定。

运营状态建模 · 决策支持 · 对账反馈闭环

Next.js · TypeScript · Supabase · Cloudflare Workers · 全程合成数据


命题

给一票货定价要同时用到三样东西,而它们存放在三个地方。其中一样在某个人的记忆里。

货代收到询价,向几家承运商问价,选一家,加上利润,发出报价——几周之后,再拿承运商实际开来的账单对账。要把这件事做好,需要同时握住三样东西:承运商今天报的价,它们从邮件里陆续进来;可比的历史报价,它们散落在文件夹里;以及这个客户该加多少点,它通常只存在某个人脑子里。

结果是这份工作被「记不记得住」卡住。经验成了瓶颈:新人没有它就报不了价;存着的默认值会悄悄偏离实际在用的数;而错误往往很晚才浮出来——老员工当周就能指出新人的错,新人却要等到一年后对账时才发现老员工的错。

NextJune 建立在关于这件事的一个想法上:员工是做出决定的人,而系统负责记忆,并主动标明需要注意的东西。它从不替人选承运商、也不替人定利润。它只保证:本来需要有人记住的东西,在做决定的那一刻已经在屏幕上了。


我给范围定的规矩

要么解决通用问题,要么明说它不是。

一个从行业经验里长出来的工具,最容易犯的错是把某一家公司的习惯编码进去,然后管它叫行业。所以需求里带着一条明确的指令:只做中小型货代共有的部分,而且如果某条需求最后发现只适用于一家公司,就说出来并停下,不要继续做。

这条规矩也是数据全程合成、从零编写而非取自任何地方的原因。它同样是下面那个演示可以完全开放的原因:里面没有任何需要保护的东西,所以整个系统都可以被查看和质疑。


这份工作的形状

报价不是一张表单。它是一张中间夹着等待的表单。

承运商通常几小时内回复。于是这条动线是收集 → 问 → 等 → 回来 → 定价,而最有意思的那个时刻,恰好是最容易丢掉的那个:没回来的那一条询价。超过半天就该有人去追,而之所以经常没人追,是因为没有任何东西在记着。

把报价建模成一张表单的系统,建模的是另一份工作。这一个建模了那段等待。


取舍

下面每一条的存在,都是为了把某样东西从人脑里拿出来,放到屏幕上。

建模

报价先于它的价格存在

你问了三家承运商,一家都没回。表格把这件事记成「什么都没有」——那几行根本不存在,而这和「从来没问过」在数据上毫无区别。

这里,询价从发出的那一刻就存在:没有费率,但有一个明确的状态——待回复、已回复、未回应、已婉拒。一个空格子原本只能表达一件事,现在是四件不同的事实。而其中只有一部分意味着该去催人。

一张草稿报价,三家承运商已询价但都未回复,费率栏显示「收到后填入」
3 家询价 · 0 家回复 · 3 家待回。费率栏不是空的,它是在等——而承运商下面那条钉住的备注,正是平时只存在于某个人经验里的东西。
算出来

该问谁是算出来的,不是想起来的

知道某条航线该找哪几家承运商,恰好是那种要积累很多年、又从来不会被写下来的知识。它同时也是可以推导的:它就在「历史上谁真的被选中了」里面。

系统按航线从历史里推荐承运商,按被选中的次数打分,再为「没回复」和「后来对账出现争议」各扣一些。权重刻意很粗,不做任何拟合——一次争议比一次沉默更贵,但两者都不能把一个在这条线上被反复选中的承运商完全抹掉。 这个分数只是检索辅助,不是自动选择承运商的模型。

有一个细节是跑起来才发现的。卡车航线如果要求取货和送达两端邮编都匹配,几乎永远匹配不上,因为客户那一端每票都在变。只有机场那一端是稳定的锚点。这是一个发现,不是一个设计。

显形

近期利润率压过历史平均

每个客户都有一个存着的默认加价。它会悄悄过时:费率变了,报价的人跟着调了,但没人回去更新那条记录。全历史的平均值会把这件事盖住——真正说明「现在实际收多少」的,是最近那几张报价。

所以显示的加价率向最近几张倾斜;一旦它和存着的默认值差出两个点以上,表单就会说出来,并把两种修正都放在一次点击之内。它不替人选。

报价的定价步骤:存着的默认值 15%,近期实际 18.4%,并提示可以用 18.4% 或更新客户记录
存着的是 15%,最近实际按 18.4% 报。这一行就是整个论点:一件本来要靠同事知道的事,在它要紧的那一刻被说了出来——而选哪个,仍然是人的决定。
打通

对账是定价的输入,不是会计的杂务

承运商的账单在发运几周后才到。对账意味着拿它和当初的报价比——或者,当货物普通、根本没发询价时,和存档的合同价比。合同价是基准;询价是留给那些落在基准之外的货的。

这意味着差异很少只是一个记账错误。它可能是到港复重。也可能是一张没人刷新过的费率卡,正在悄悄地让这条线上的每一次报价都算错,直到有人注意到。对账是这些东西浮出来的地方,所以它被接回了报价,而不是停在会计那里。

供应商对账面板:标记了差异,提示存档合同价可能已过期,并提供一键复制扯皮邮件
实际开票低于存档的合同价——它可能已经过期了。这一票少了 $601.71,因为承运商收得比存着的费率还便宜。这说明存着的那个是错的,而且一直在抬高这条线上的每一次报价。系统注意到了,「Update Contract Rate」就在旁边。再旁边,扯皮邮件已经写好了。
建模

估算货量和实际货量是两件不同的事实

客户是按估算询价的。打包之后,重量和尺寸会变——不是偶尔,是常态。一个允许直接覆盖估算值的系统,会丢掉对账最需要的那样东西:当初定这个价时,你是按什么在报。

所以实际货量是一个独立的确认步骤,而不是在原值上改,报价同时带着两者。特殊操作要求也是同一个道理:危险品、温控、不可堆叠、超尺寸都记在报价上,因为正是它们让合同价变得不可靠、让邮件询价变得必要。

一张标记了不可堆叠和温控要求的报价,一家航司确认可接,另一家因温控婉拒
一家确认能做 2–8°C,另一家婉拒了。婉拒被保留下来,而不是删掉那一行。下次同类货走这条线,这件事已经是已知的了。

刻意不做的部分

这东西最初的设想是十个模块。那个版本永远做不完,而一个做不完的项目,和一个被放弃的项目在外观上没有区别。

最初的草图覆盖了货代做的每一件事,而且还打算跨行业复用。它被刻意砍到了那条有头有尾的脊椎:询价进来,发票出去。客户关系、报价、开票、供应商对账留下了;文件生成、角色权限、产品目录、分享中心没有——那些属于那个通用平台的设想,而它和一个货代报价工具是两个不同的产品,真要做也应该当作另一个产品来做。

对账与收付款仍然列为计划中,因为它们延伸的是同一条脊椎:供应商报了多少、后来开了多少、以及真正流动的那笔钱。


这是怎么做出来的

我基于货代领域经验定义了运营模型、产品规则、状态流转、定价行为、对账逻辑、边界情况和验收标准。代码实现由 AI coding agent 生成;我审查系统行为、测试边界情况并推动修订。所有演示数据都是为这个项目专门创建的合成数据。

现状

部署在 Cloudflare Workers 上,后面是 Supabase。金额在每一步都舍入到分,使应用和数据库永远不会对同一张报价持有不同的数字,并有十七个测试直接 import 真实模块而不是重述它。每次推送都会跑 lint、类型检查、测试和一次真实构建。

下面那个演示是一个真实账号,带着它自己的种子历史——虚构的客户、虚构的承运商、虚构的货——由分隔任意两个用户的同一套行级安全策略隔离,并且每晚重建。里面的任何东西你都可以改。

Built by Emilie Zhang actaurus.com