Flyttsmart case
The technical case that got me hired. It matches moving customers to offers and explains every no.
The problem
Why it exists
Flyttsmart's case: take a customer's moving data and a batch of partner offers, like moving help, broadband, alarms and cleaning. Decide what the customer is eligible for, explain why the rest fell out, and rank the matches. The data comes in through n8n from forms and partners, so any field can be missing or broken.
The obvious version answers yes or no. The useful version can also say "I can't tell yet, ask the customer". It gives the same answer every time for the same input, and one broken offer never takes down the whole batch.
How it works
One batch through matchOffers()
Step 1 of 6 · External system
n8n payload
An n8n flow collects the customer's moving data and the offers, then calls the function. n8n handles the plumbing. The decisions live in tested code.
Decisions
What I chose, and what I didn't
Three values, not two
Each condition is pass, fail or unknown. Folding unknown into fail would hand the sales team a false "not eligible" when the honest answer is "ask the customer".
Rejected
Boolean eligibility. Simpler, and wrong for every customer who just skipped a field.
Rules are data, not if-statements
Conditions are typed objects like { type: "maxLivingArea", sqm: 70 }, evaluated by one function. A new rule type is a new variant in a union, and the compiler's exhaustiveness check forces the matching case before it builds.
Rejected
Business rules in the n8n flow's IF and Switch nodes. They can't be unit tested and get hard to read fast.
Category before price
Price only compares within a service category. Broadband at 399 kr isn't a better offer than moving help at 3,800 kr. It's a different purchase. Sorting them together buries what the customer came for.
Rejected
Cheapest first across everything.
Zod at the edge, not instead
Looking back, Zod could replace about 55 of 367 lines of shape checks plus the hand-written date parser. But to Zod, {} is a perfectly valid customer. The whole unknown track starts after "well-formed", so the three-valued core stays.
Rejected
Replacing the core with a validation library. Zod answers "is this well-formed?". The hard question here is "it's well-formed, and I still can't decide".
By the numbers
What it adds up to
- tests, all under a second
- 33
- runtime dependencies
- 0
- possible answers for every condition
- 3
- sort keys, the last one unique
- 4
What I learned
I tested through the public function, not the guards. The tests check buckets and reason codes from matchOffers(), not the validation code. I could swap the whole validation layer for Zod tomorrow and the suite would still tell me whether the answers are right.