← All work
CompletedTechnical case · now a part-time job

Flyttsmart case

The technical case that got me hired. It matches moving customers to offers and explains every no.

TypeScriptNode.jsJestn8n

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.