payload-shipping-classes
v1.0.0
Published
Weight, dimensions and a shipping class on Payload products and variants, stored in one canonical unit.
Downloads
53
Maintainers
Readme
payload-shipping-classes
Gives Payload products and variants a weight, a size and a shipping class, stored in one canonical unit, so that whatever prices the delivery can add a cart up without having to ask what the numbers mean.
- Works with
@payloadcms/plugin-ecommerceand with any collection that holds products - Weight entered in
g,kg,ozorlb, dimensions inmm,cm,morin, stored in grams and millimetres - A variant overrides its product, measurement by measurement
- Exports a pure function that totals a cart, so a rate calculator can read the result without installing this package
- No runtime dependencies
- No admin components, so it survives minor releases
Install
Requires Payload 3.88 or newer and @payloadcms/plugin-ecommerce 3.88 or newer. Verified against Payload 3.88.0 with the official plugin installed.
pnpm add payload-shipping-classesimport { shippingClassesPlugin } from 'payload-shipping-classes'
export default buildConfig({
plugins: [
shippingClassesPlugin({
weightUnit: 'kg',
dimensionUnit: 'cm',
}),
],
})A product now has a Weight (kg) field, a Dimensions (cm) group and a shipping class. A variant has the same three, and whatever it fills in wins over the product.
What was measured
Measured on 19 August 2026 against the published contents of @payloadcms/[email protected].
A product has no weight and no size. These are all the fields the official plugin puts on a product and on a variant:
| Collection | Fields the plugin adds |
| --- | --- |
| products | inventory, enableVariants, variantTypes, variants, and priceIn<CODE> with priceIn<CODE>Enabled for each supported currency |
| variants | title, product, options, inventory, and priceIn<CODE> with priceIn<CODE>Enabled for each supported currency |
Searching the whole published package for weight finds two occurrences, both of them font-weight in a stylesheet. dimension does not occur at all. There is nothing on a product that describes the parcel it will travel in.
A number without a unit cannot be added up. A single weight field of 2 means two grams to one shop and two pounds to the next, and it changes meaning the day a shop switches unit. So the entered value is kept as the shop typed it, and a second, hidden value is written next to it in one fixed unit. Every calculation reads the hidden one.
| Entered | Unit | Stored |
| --- | --- | --- |
| 1.25 | kg | 1250 g |
| 750 | g | 750 g |
| 5 | lb | 2268 g |
| 1 | oz | 28 g |
| 2.5 | cm | 25 mm |
| 12 | in | 305 mm |
Conversion uses the international avoirdupois pound of 453.59237 g and the inch of exactly 25.4 mm, then rounds to the nearest gram or millimetre. The largest error a conversion can introduce is half a gram, which is finer than any carrier weighs.
Totals stay on the integers. Adding ten weights of 0.001 kg in JavaScript gives 0.010000000000000002, and three of 0.1 kg give 0.30000000000000004. Adding ten of 1 g gives 10. Conversion rounds once, when the value is stored, and every addition after that is between whole numbers.
Resolution order
For each of the three measurements, independently, in this order and no other:
- the variant's own value, when the variant is present and its value is a finite number of zero or more
- otherwise the product's value, under the same test
- otherwise nothing, and the line is reported as having no value for that measurement
Two consequences follow, and both are deliberate:
- A variant weight of
0is an override. A weightless variant of a heavy product weighs nothing. Onlynull,undefined, a missing field, a value that is not a number, or a negative number falls through to the product. - The dimensions group is resolved as a whole, not face by face. A variant that sets only
lengthoverrides the product's length, width and height together, leaving width and height empty. Partial groups are never merged, because a box with one dimension from the variant and two from the product is not a box anyone measured.
resolveShippingLine reports where each value came from, as 'variant', 'product' or 'none', so a shop can see the rule applied rather than infer it.
Totalling a cart
import { totalCartShipping } from 'payload-shipping-classes'
const totals = totalCartShipping([
{ product: greenMug, quantity: 2, variant: largeGreenMug },
{ product: giftCard, quantity: 1 },
]){
weightGrams: 1400,
volumeMm3: 2400000,
quantity: 3,
shippingClasses: ['fragile'],
unitsWithoutWeight: 1,
lines: [ ... ]
}unitsWithoutWeight is the point of the function. A cart that weighs 1400 g and a cart that weighs 1400 g plus one item nobody ever weighed are different carts, and a rate calculator that cannot tell them apart will quote a price for a parcel it has not seen.
The returned shape is the whole contract. A consumer needs weightGrams, shippingClasses and unitsWithoutWeight, all three plain JSON, and needs no import from this package to read them.
Options
| Option | Default | Meaning |
| --- | --- | --- |
| canonicalDimensionsFieldName | 'dimensionsMm' | Hidden group holding the dimensions in millimetres |
| canonicalWeightFieldName | 'weightGrams' | Hidden field holding the weight in grams |
| dimensions | true | Set to false to add the weight and the class but no dimensions |
| dimensionsFieldName | 'dimensions' | Group holding the entered dimensions |
| dimensionUnit | 'cm' | One of mm, cm, m, in |
| disabled | false | Stops the canonical values being recalculated but keeps every field, so the database keeps its shape |
| productsSlug | 'products' | Slug of the products collection |
| shippingClassesOverrides | {} | Merged over the generated collection, applied last |
| shippingClassesSlug | 'shipping-classes' | Collection holding the classes |
| shippingClassFieldName | 'shippingClass' | Relationship field on products and variants |
| variantsSlug | 'variants' | Slug of the variants collection. Ignored when the shop has no variants |
| weightFieldName | 'weight' | Field holding the entered weight |
| weightUnit | 'kg' | One of g, kg, oz, lb |
An unusable value is replaced by its default rather than applied. An unknown unit falls back to kg or cm, and a name given as an empty string falls back to its default name.
What it adds to your database
| Collection | Field | Type | Notes |
| --- | --- | --- | --- |
| your products collection | weight | number | entered in the configured unit, minimum 0 |
| your products collection | weightGrams | number | whole grams, hidden and read only, written by the plugin |
| your products collection | dimensions | group of length, width, height | entered in the configured unit, minimum 0 |
| your products collection | dimensionsMm | group of length, width, height | whole millimetres, hidden and read only, written by the plugin |
| your products collection | shippingClass | relationship | indexed, optional |
| your variants collection | the same five | | added only when the collection exists |
| shipping-classes | name | text | required |
| shipping-classes | slug | text | required, unique, indexed. Filled from the name when left empty |
| shipping-classes | description | textarea | optional |
The canonical values are recalculated in a beforeChange hook on every save, from the entered value in the incoming data, or from the stored one when a partial update leaves it out. Clearing a weight clears the canonical weight to null rather than to 0.
The shipping-classes collection is readable by anyone and writable by any authenticated user. Pass shippingClassesOverrides with your own access to narrow that to your administrators.
Without payload-shipping-rates
This package prices nothing. On its own it records a weight, a size and a class, and totals a cart on request. Nothing else in a Payload install reads those fields until something is installed that does.
payload-shipping-rates reads weightGrams and shippingClass by those names, but it does not import this package and does not require it. Either package can be installed without the other.
Honest limits
Changing a unit does not rewrite what is already stored. The canonical value is recalculated when a document is saved. If a shop switches weightUnit from kg to lb with products already in the database, those products keep a canonical weight calculated under the old unit until each one is saved again. Switch the unit before entering data, or resave the catalogue afterwards.
Existing products have no weight. The plugin only writes a canonical value when a document is saved. Products already in the database keep an empty weight and an empty weightGrams until they are next edited.
Whole grams and whole millimetres. A weight is stored to the nearest gram and a length to the nearest millimetre. A shop weighing single seed beads in ounces will see the rounding; a shop shipping parcels will not.
Dimensions are recorded, not used. volumeMm3 is the sum of length times width times height for every line that has all three. It is not a volumetric weight, not a girth, and not a packing calculation. Nothing here decides which box the order goes in.
One class per product. A product or a variant carries a single shipping class. There is no notion of a product being both fragile and oversized, and no per class surcharge here, because pricing does not live in this package.
The slug is what gets referenced. A rate calculator matches classes by slug. Renaming a class is safe; changing its slug breaks anything that referred to the old one.
License
MIT. Copyright George Vasiliades, https://github.com/Poseidonas
