Most furniture retailers discover their SKU data is broken the hard way: a customer orders a 3‑piece sectional, the system shows "12 available," and then the warehouse finds 12 left‑arm chaises, 4 right‑arm pieces, and 9 armless middles. The parent product says in stock. The actual set is not. That gap between "we have cartons" and "we can ship a complete sofa" is where availability lies get born.
If you've already read the minimal SKU data spec for bulky furniture, this is the companion that fills the part that spec left open on purpose: the actual machine‑readable schema, the validation rules, the math for computing parent availability from carton stock, and the carrier‑mapping thresholds. No philosophy here. This is the thing you hand to your engineer, your 3PL integration person, and your supplier's data team so nobody has to guess what weight_kg means or whether dims is in centimeters.
Everything below targets the bulky furniture SKU JSON schema problem specifically — multi‑carton products where the sellable unit and the shipped unit are not the same thing.
The core data model: product vs carton
The single most important decision is separating the product (what the customer buys) from the carton (what physically moves). A sectional is one product. It ships as three cartons. A dining set is one product that might ship as five cartons across two pallets. Flatten these together in your schema and you will never compute availability correctly.
-
A product record describes the sellable thing, its components, and how many of each carton make a complete set.
-
A carton record describes one physical box
dimensions, weight, barcode, and which product(s) it belongs to.
Inventory is counted at the carton level. Availability is computed at the product level. That one rule prevents most false‑availability problems in this category.
The JSON Schema (Draft‑07)
Here's the product schema. It's deliberately strict — additionalProperties: false so nobody quietly injects undocumented fields that break downstream parsers.
Eliminate inventory headaches and order delays.
Hosyly streamlines your furniture orders and stock management for seamless store operations.
- Real-time inventory tracking
- Automated order processing
- Customer notifications & engagement
No credit card required
{ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://example.com/schemas/furniture-product.json", "title": "FurnitureProduct", "type": "object", "additionalProperties": false, "required": ["productid", "sku", "title", "category", "components", "status"], "properties": { "productid": { "type": "string", "pattern": "^PRD-[0-9]{6}$", "description": "Internal product identifier" }, "sku": { "type": "string", "pattern": "^[A-Z0-9]{3,6}-[A-Z0-9]{2,8}$", "description": "Customer-facing SKU, e.g. SOFA-SECT3L" }, "title": { "type": "string", "minLength": 3, "maxLength": 120 }, "category": { "type": "string", "enum": ["sofa", "sectional", "diningset", "bed", "wardrobe", "table", "chair", "storage", "other"] }, "status": { "type": "string", "enum": ["active", "discontinued", "preorder", "hold"] }, "assemblyrequired": { "type": "boolean" }, "components": { "type": "array", "minItems": 1, "items": { "type": "object", "additionalProperties": false, "required": ["cartonsku", "qtyperset"], "properties": { "cartonsku": { "type": "string", "pattern": "^CTN-[A-Z0-9]{4,10}$" }, "qtyperset": { "type": "integer", "minimum": 1, "maximum": 20 }, "role": { "type": "string", "enum": ["primary", "accessory", "hardware", "optional"] } } } }, "deliveryserviceprofile": { "type": "string", "enum": ["parcel", "ltlcurbside", "ltlthreshold", "twopersonroomofchoice", "whiteglove"] }, "leadtime_days": { "type": "integer", "minimum": 0, "maximum": 365 } } }
And the carton schema:
{ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://example.com/schemas/furniture-carton.json", "title": "FurnitureCarton", "type": "object", "additionalProperties": false, "required": ["cartonsku", "gtin", "dimscm", "weightkg"], "properties": { "cartonsku": { "type": "string", "pattern": "^CTN-[A-Z0-9]{4,10}$" }, "gtin": { "type": "string", "pattern": "^[0-9]{12,14}$", "description": "UPC/EAN/GTIN-14, numeric only" }, "dimscm": { "type": "object", "additionalProperties": false, "required": ["length", "width", "height"], "properties": { "length": { "type": "number", "minimum": 1, "maximum": 400 }, "width": { "type": "number", "minimum": 1, "maximum": 300 }, "height": { "type": "number", "minimum": 1, "maximum": 300 } } }, "weightkg": { "type": "number", "minimum": 0.1, "maximum": 150 }, "stackable": { "type": "boolean" }, "freight_class": { "type": "string", "enum": ["50", "55", "60", "65", "70", "77.5", "85", "92.5", "100", "110", "125", "150", "175", "200"] } } }
A note on why dimscm and weightkg carry units in the field name: integrations break when one supplier sends inches and another sends centimeters into a field just called length. Put the unit in the key. It's ugly. It's also unambiguous, and ugly‑but‑unambiguous wins every time in supplier data.
Example payloads
A 3‑carton sectional product record:
{ "productid": "PRD-004821", "sku": "SECT-3LCHS", "title": "Halden 3-Piece Left-Chaise Sectional", "category": "sectional", "status": "active", "assemblyrequired": true, "deliveryserviceprofile": "twopersonroomofchoice", "leadtimedays": 21, "components": [ { "cartonsku": "CTN-HAL3L01", "qtyperset": 1, "role": "primary" }, { "cartonsku": "CTN-HAL3L02", "qtyperset": 1, "role": "primary" }, { "cartonsku": "CTN-HAL3L03", "qtyper_set": 1, "role": "primary" } ] }
One of its cartons:
{ "cartonsku": "CTN-HAL3L01", "gtin": "0081234500011", "dimscm": { "length": 165, "width": 95, "height": 88 }, "weightkg": 41.5, "stackable": false, "freightclass": "150" }
Validation rules that actually catch bad data
Schema validation gets you type safety. It doesn't catch the stuff that causes false availability. These cross‑field and semantic rules do.
| Rule | Check | Why it matters |
|---|---|---|
| GTIN checksum | Validate the GTIN check digit, not just the regex | A numeric string that passes the pattern can still be a typo'd barcode that scans to nothing |
| Carton reference integrity | Every components[].carton_sku must exist as a carton record | Products referencing phantom cartons compute as "available" with zero real stock |
| Duplicate carton in set | No carton_sku appears twice in one product's components | Doubles the apparent carton need, understates availability |
| Oversize vs profile | If any carton length > 220 or weightkg > 68, deliveryservice_profile cannot be parcel | Parcel carriers reject oversize; this prevents checkout promising parcel delivery on a sofa |
| Threshold weight cap | twopersonroomofchoice requires at least one carton > 32 kg OR total set weight > 50 kg | Lets you flag products mis‑tagged as single‑person deliverable |
| Dimensional sanity | height <= length is NOT enforced, but any dimension < 5 cm on a primary carton triggers review | Catches unit errors (meters entered as cm, or vice versa) |
| Freight class presence | Required when deliveryserviceprofile is ltl* or whiteglove | LTL rating fails without a class; defaults silently overcharge |
The oversize‑vs‑profile rule is the one most retailers skip and most regret. If a sectional carton gets tagged parcel by mistake, your site quotes something like $12 shipping, the customer pays, and now you're eating $180 in LTL freight on every one of those orders until someone catches it in a monthly reconciliation.
Computing parent ATP from carton inventory
Available‑to‑promise for a multi‑carton product is not the smallest carton count. It's the number of complete sets you can build, accounting for how many of each carton a set needs, minus what's reserved and what's genuinely sellable from in‑transit.
setsavailable = MIN over all primary components of: floor( (onhand[carton] - reserved[carton]) / qtyperset[carton] )
SELECT p.productid, MIN( FLOOR( (inv.onhand - inv.reserved) / c.qtyperset ) ) AS setsavailable FROM productcomponents c JOIN inventory inv ON inv.cartonsku = c.cartonsku JOIN products p ON p.productid = c.productid WHERE c.role = 'primary' GROUP BY p.product_id;
-
It only counts
primarycartons. Accessory and hardware cartons are usually abundant and shouldn't gate set availability — but you track them separately so you can flag the rare case where you're actually out of leg hardware. -
It subtracts
reservedbefore dividing. Reserved stock is allocated to open orders. Counting it as available is the classic double‑sell. -
FLOORhandles partial sets. If you have 7 of carton A and 5 of carton B, each needing 1 per set, you can build 5 sets. The 2 extra A cartons are real inventory but not sellable as complete product.
For in‑transit handling, don't blend it into on_hand. Keep it a separate column and expose it as a distinct ATP tier:
SELECT p.productid, MIN(FLOOR((inv.onhand - inv.reserved) / c.qtyperset)) AS setsnow, MIN(FLOOR((inv.onhand - inv.reserved + inv.intransit) / c.qtyperset)) AS setswithintransit FROM productcomponents c JOIN inventory inv ON inv.cartonsku = c.cartonsku JOIN products p ON p.productid = c.productid WHERE c.role = 'primary' GROUP BY p.product_id;
setsnow drives "ships this week." setswith_intransit drives "available for backorder with date." Mixing them is exactly how a customer gets a two‑day promise on a sofa that's still on a container.
The partial‑set trap
The most common real pattern: a supplier ships carton 1 and carton 2 of a sectional, but carton 3 is on a later production run. Receiving scans in two of three, inventory shows positive counts on both, and any logic that forgets to check all three components will happily mark the set available. The MIN across all primary components — including the one sitting at zero — is what keeps that set correctly at zero sellable.
Inventory queries for multi‑carton products should always follow this general sequence:
-
Pull on‑hand and reserved counts at the carton level.
-
Compute net available per carton (
on_hand - reserved). -
Divide each net count by that carton's
qtyperset. -
Take the
MINacross all primary components. -
FLOORthe result to whole sets only. -
Separately compute
setswithintransitusing the same logic but addingin_transitbefore dividing.
Running these as two separate queries — not one blended number — is what gives your checkout a figure it can actually stand behind.
Visualize this as a workflow:
Keep the queries separate and label the ATP tiers clearly so UX and ops know which number is which.
Carrier / delivery_service_profile mapping thresholds
The deliveryserviceprofile enum isn't cosmetic. It decides which carrier type gets the order and what you quote at checkout. Map it from carton physical attributes using explicit thresholds so it's derivable, not hand‑typed.
| Profile | Trigger conditions (any carton in set) | Typical handling |
|---|---|---|
parcel | All cartons ≤ 68 kg, longest dim ≤ 150 cm, girth under carrier max | Standard ground parcel |
ltl_curbside | Any carton 68–150 kg OR longest dim 150–220 cm | Freight dropped at curb |
ltl_threshold | Same weight/dim as curbside, but product flagged for indoor drop | Brought inside first dry space |
twopersonroomofchoice | Set total weight > 50 kg OR any single carton > 32 kg AND needs placement | Two‑person crew, room placement |
white_glove | assembly_required = true AND set total weight > 50 kg | Delivery + assembly + packaging removal |
Keep the thresholds in a config table, not hardcoded. Carriers change their oversize cutoffs, and you want to re‑derive profiles in a batch job rather than re‑edit code. When a carrier drops its parcel max from 150 cm to 120 cm, you change one row and re‑run the mapping.
Automated checks and anomaly thresholds
Validation runs at ingest. Anomaly detection runs continuously, because data rots after it's loaded — suppliers re‑pack, warehouses re‑measure, and someone always edits a record at 4pm on a Friday.
-
Negative sellable sets — any product where
on_hand - reserved < 0on a primary carton. Hard alert, immediate. This means you've oversold. -
Dimension drift — carton dims changed by more than 10% from the last recorded value. Soft alert for review; usually a re‑measure or a supplier packing change.
-
Weight outliers — a carton whose
weight_kgis more than 3× or less than 0.3× the median weight of cartons in its category. Catches the "41.5 entered as 415" class of error. -
Orphaned cartons — carton records referenced by no product, or product components pointing at cartons that don't exist. Weekly digest.
-
Profile mismatch — any product where the stored
deliveryserviceprofiledisagrees with what the threshold mapping would derive. This is your single best signal that freight is being mis‑quoted.
A simple alert tiering keeps the noise manageable:
-
Hard (page someone) oversell, phantom carton on an active product with open orders.
-
Soft (queue for daily review) dimension drift, weight outlier, profile mismatch.
-
Digest (weekly) orphaned cartons, discontinued products still carrying stock.
Wiring everything to the same Slack channel is the mistake most teams make. Within a week people mute it, and the one oversell alert that actually mattered gets buried under forty dimension‑drift notices.
Onboarding ownership and SLAs
Schema and math only hold if someone owns the data at the point it enters. A tight ownership map, even for a small team:
| Field group | Owner | SLA |
|---|---|---|
| Carton dims + weight | Supplier at onboarding; warehouse re‑verifies first receipt | Supplier provides pre‑ship; warehouse confirms within 2 business days of first receipt |
| GTIN / barcodes | Supplier | Provided and checksum‑validated before first PO ships |
| Components / qtyperset | Merchandising | Signed off before product goes active |
| deliveryserviceprofile | Derived by system, reviewed by ops | Ops reviews any mismatch flag within 1 business day |
| Inventory counts | Warehouse | Real‑time on receipt/pick; cycle‑count reconcile monthly |
The sign‑off that gets skipped most is the first‑receipt re‑verification of carton weight and dims. Suppliers send spec‑sheet numbers; actual packed cartons differ. One physical measurement on the first shipment prevents a year of wrong freight quotes.
Audit KPIs and cadence
Track these so you know the data is healthy, not just assume it:
-
Profile mismatch rate — % of active products where derived profile ≠ stored profile. Target under 2%. Review weekly.
-
Oversell incidents — count of orders placed against negative sellable sets. Target zero. Review on occurrence.
-
First‑receipt measurement compliance — % of new cartons physically verified within SLA. Target above 90%. Review monthly.
-
Orphan rate — orphaned cartons ÷ total cartons. Keep under 1%. Review monthly.
-
Phantom‑set catches — number of products correctly held at zero availability due to a missing component carton.
This is a positive metric — it tells you the ATP math is doing its job.
End‑to‑end test plan with sample data
Before this goes anywhere near production, run it against a known 3‑carton sectional and verify every layer.
Sample fixtures: the Halden sectional (PRD-004821) with three primary cartons, each qtyperset = 1.
Test cases:
-
Full set available. Load onhand = 5 for all three cartons, reserved = 0. Expect
setsnow = 5. -
Partial set. Load onhand = 5, 5, 0. Expect
setsnow = 0. This is the critical one — two cartons stocked, one empty, must return zero. -
Reserved stock. Load onhand = 5, 5, 5, reserved = 2 on carton 2. Expect
setsnow = 3. -
In‑transit split. onhand = 5, 5, 0; intransit = 0, 0, 10. Expect
setsnow = 0,setswith_intransit = 5. -
Oversell guard. on_hand = 1, 1, 1; attempt to reserve 2 sets. Expect reservation rejected and a hard alert.
-
Profile mapping. Carton 1 at 41.5 kg, assembly required, set weight > 50 kg. Expect derived profile
white_glove; if stored profile saysparcel, expect a mismatch flag. -
Validation rejection. Submit a carton with
weight_kg = 415. Expect weight‑outlier flag and schema still passing (it's under 150? no — 415 exceeds max, so schema rejects). Confirm the hard cap catches it.
If all seven pass against the sample sectional, the schema, the ATP math, and the mapping logic are consistent with each other — which is the whole point of writing them in one spec.
Where this fits
This spec is intentionally narrow: one data model, one set of validation rules, one ATP formula, one mapping table. It doesn't cover how to govern the data over time or reconcile it across systems — that's a different problem. What it does is remove the ambiguity that makes multi‑carton furniture availability unreliable in the first place.
If you implement nothing else from this, implement the MIN across all primary components and the first‑receipt carton measurement. Those two changes alone eliminate the majority of "it said available but we can't ship it" tickets that plague bulky furniture fulfillment — and they give your checkout a number it can actually stand behind.
Ready to elevate your furniture business?
Join 500+ furniture retailers using Hosyly to increase efficiency, improve customer satisfaction, and grow revenue.