466 lines
19 KiB
HTML
466 lines
19 KiB
HTML
<title>Shipping Feature Survey</title>
|
|||
|
|
<style>
|
||
|
|
:root {
|
||
|
|
--paper: #FAFAF7;
|
||
|
|
--ink: #1C1C1A;
|
||
|
|
--muted: #6B6B63;
|
||
|
|
--accent: #2F5D50;
|
||
|
|
--accent-soft: #E4EDE9;
|
||
|
|
--good: #3F7A5C;
|
||
|
|
--good-soft: #E6F0EA;
|
||
|
|
--warn: #B8863B;
|
||
|
|
--warn-soft: #F5ECDC;
|
||
|
|
--miss: #A14B3B;
|
||
|
|
--miss-soft: #F5E5E0;
|
||
|
|
--hairline: #E4E2DB;
|
||
|
|
--card: #FFFFFF;
|
||
|
|
}
|
||
|
|
|
||
|
|
:root:not([data-theme="light"]) {
|
||
|
|
@media (prefers-color-scheme: dark) {
|
||
|
|
--paper: #17181A;
|
||
|
|
--ink: #EDEBE4;
|
||
|
|
--muted: #9B9A90;
|
||
|
|
--accent: #7FBFA8;
|
||
|
|
--accent-soft: #1E2C27;
|
||
|
|
--good: #6FBF97;
|
||
|
|
--good-soft: #1B2A22;
|
||
|
|
--warn: #D9A85C;
|
||
|
|
--warn-soft: #2C2418;
|
||
|
|
--miss: #D97C68;
|
||
|
|
--miss-soft: #2E1E1A;
|
||
|
|
--hairline: #2C2D2E;
|
||
|
|
--card: #1E1F21;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
:root[data-theme="dark"] {
|
||
|
|
--paper: #17181A;
|
||
|
|
--ink: #EDEBE4;
|
||
|
|
--muted: #9B9A90;
|
||
|
|
--accent: #7FBFA8;
|
||
|
|
--accent-soft: #1E2C27;
|
||
|
|
--good: #6FBF97;
|
||
|
|
--good-soft: #1B2A22;
|
||
|
|
--warn: #D9A85C;
|
||
|
|
--warn-soft: #2C2418;
|
||
|
|
--miss: #D97C68;
|
||
|
|
--miss-soft: #2E1E1A;
|
||
|
|
--hairline: #2C2D2E;
|
||
|
|
--card: #1E1F21;
|
||
|
|
}
|
||
|
|
|
||
|
|
* { box-sizing: border-box; }
|
||
|
|
|
||
|
|
body {
|
||
|
|
background: var(--paper);
|
||
|
|
color: var(--ink);
|
||
|
|
font-family: "IBM Plex Sans", ui-sans-serif, system-ui, sans-serif;
|
||
|
|
font-size: 15.5px;
|
||
|
|
line-height: 1.55;
|
||
|
|
margin: 0;
|
||
|
|
padding: 4.5rem 1.5rem 6rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.wrap {
|
||
|
|
max-width: 780px;
|
||
|
|
margin: 0 auto;
|
||
|
|
}
|
||
|
|
|
||
|
|
header.page {
|
||
|
|
margin-bottom: 3.25rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.eyebrow {
|
||
|
|
font-family: "IBM Plex Mono", ui-monospace, monospace;
|
||
|
|
font-size: 0.72rem;
|
||
|
|
letter-spacing: 0.12em;
|
||
|
|
text-transform: uppercase;
|
||
|
|
color: var(--accent);
|
||
|
|
margin-bottom: 0.9rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
h1 {
|
||
|
|
font-family: "Fraunces", Georgia, serif;
|
||
|
|
font-weight: 560;
|
||
|
|
font-size: clamp(2.1rem, 4.5vw, 2.65rem);
|
||
|
|
line-height: 1.08;
|
||
|
|
letter-spacing: -0.01em;
|
||
|
|
margin: 0 0 0.9rem;
|
||
|
|
text-wrap: balance;
|
||
|
|
}
|
||
|
|
|
||
|
|
.dek {
|
||
|
|
color: var(--muted);
|
||
|
|
max-width: 60ch;
|
||
|
|
font-size: 1.02rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.dek strong {
|
||
|
|
color: var(--ink);
|
||
|
|
font-weight: 600;
|
||
|
|
}
|
||
|
|
|
||
|
|
.legend {
|
||
|
|
display: flex;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
gap: 0.6rem;
|
||
|
|
margin-top: 1.6rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.chip {
|
||
|
|
display: inline-flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 0.4rem;
|
||
|
|
font-family: "IBM Plex Mono", ui-monospace, monospace;
|
||
|
|
font-size: 0.72rem;
|
||
|
|
letter-spacing: 0.04em;
|
||
|
|
padding: 0.28rem 0.6rem;
|
||
|
|
border-radius: 3px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.chip.have { background: var(--good-soft); color: var(--good); }
|
||
|
|
.chip.partial { background: var(--warn-soft); color: var(--warn); }
|
||
|
|
.chip.missing { background: var(--miss-soft); color: var(--miss); }
|
||
|
|
|
||
|
|
section.category {
|
||
|
|
margin-top: 3rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.cat-head {
|
||
|
|
display: flex;
|
||
|
|
align-items: baseline;
|
||
|
|
gap: 0.85rem;
|
||
|
|
border-bottom: 1px solid var(--hairline);
|
||
|
|
padding-bottom: 0.7rem;
|
||
|
|
margin-bottom: 1.1rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.cat-num {
|
||
|
|
font-family: "Fraunces", Georgia, serif;
|
||
|
|
font-size: 1.05rem;
|
||
|
|
color: var(--accent);
|
||
|
|
font-variant-numeric: tabular-nums;
|
||
|
|
min-width: 1.6rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.cat-head h2 {
|
||
|
|
font-family: "Fraunces", Georgia, serif;
|
||
|
|
font-weight: 500;
|
||
|
|
font-size: 1.28rem;
|
||
|
|
margin: 0;
|
||
|
|
letter-spacing: -0.005em;
|
||
|
|
}
|
||
|
|
|
||
|
|
.cat-note {
|
||
|
|
color: var(--muted);
|
||
|
|
font-size: 0.86rem;
|
||
|
|
margin: 0 0 1.2rem;
|
||
|
|
max-width: 62ch;
|
||
|
|
}
|
||
|
|
|
||
|
|
.feature {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: 1fr auto;
|
||
|
|
gap: 0.3rem 1rem;
|
||
|
|
padding: 1.05rem 0;
|
||
|
|
border-bottom: 1px solid var(--hairline);
|
||
|
|
align-items: start;
|
||
|
|
}
|
||
|
|
|
||
|
|
.feature:last-child { border-bottom: none; }
|
||
|
|
|
||
|
|
.f-name {
|
||
|
|
font-weight: 600;
|
||
|
|
font-size: 0.98rem;
|
||
|
|
}
|
||
|
|
|
||
|
|
.f-status {
|
||
|
|
font-family: "IBM Plex Mono", ui-monospace, monospace;
|
||
|
|
font-size: 0.68rem;
|
||
|
|
letter-spacing: 0.06em;
|
||
|
|
text-transform: uppercase;
|
||
|
|
padding: 0.22rem 0.55rem;
|
||
|
|
border-radius: 3px;
|
||
|
|
white-space: nowrap;
|
||
|
|
height: fit-content;
|
||
|
|
}
|
||
|
|
|
||
|
|
.f-status.have { background: var(--good-soft); color: var(--good); }
|
||
|
|
.f-status.partial { background: var(--warn-soft); color: var(--warn); }
|
||
|
|
.f-status.missing { background: var(--miss-soft); color: var(--miss); }
|
||
|
|
|
||
|
|
.f-note {
|
||
|
|
grid-column: 1 / -1;
|
||
|
|
color: var(--muted);
|
||
|
|
font-size: 0.87rem;
|
||
|
|
margin-top: 0.15rem;
|
||
|
|
max-width: 66ch;
|
||
|
|
}
|
||
|
|
|
||
|
|
.f-note code {
|
||
|
|
font-family: "IBM Plex Mono", ui-monospace, monospace;
|
||
|
|
font-size: 0.82em;
|
||
|
|
background: var(--accent-soft);
|
||
|
|
color: var(--accent);
|
||
|
|
padding: 0.08em 0.35em;
|
||
|
|
border-radius: 3px;
|
||
|
|
}
|
||
|
|
|
||
|
|
footer.page {
|
||
|
|
margin-top: 4rem;
|
||
|
|
padding-top: 1.5rem;
|
||
|
|
border-top: 1px solid var(--hairline);
|
||
|
|
color: var(--muted);
|
||
|
|
font-size: 0.82rem;
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
gap: 1rem;
|
||
|
|
flex-wrap: wrap;
|
||
|
|
}
|
||
|
|
|
||
|
|
footer.page a { color: var(--accent); }
|
||
|
|
|
||
|
|
@media (max-width: 560px) {
|
||
|
|
body { padding: 3rem 1.1rem 4rem; }
|
||
|
|
.feature { grid-template-columns: 1fr; }
|
||
|
|
.f-status { justify-self: start; }
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400..600&family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap">
|
||
|
|
|
||
|
|
<div class="wrap">
|
||
|
|
|
||
|
|
<header class="page">
|
||
|
|
<div class="eyebrow">boboko / shipping · competitive survey</div>
|
||
|
|
<h1>What shipping elsewhere can do that boboko can't yet</h1>
|
||
|
|
<p class="dek">
|
||
|
|
A feature-by-feature pass across Shopify, WooCommerce, and PrestaShop's shipping
|
||
|
|
layer — sourced, not recalled from memory — checked against what
|
||
|
|
<strong>Lunar core's <code>ShippingManifest</code></strong> and the
|
||
|
|
<strong><code>lunarphp/table-rate-shipping</code></strong> add-on actually support
|
||
|
|
today, and what's actually wired up in boboko-core and 3dealer right now.
|
||
|
|
For deciding what to design next, not a build order.
|
||
|
|
</p>
|
||
|
|
<div class="legend">
|
||
|
|
<span class="chip have">● have</span>
|
||
|
|
<span class="chip partial">◐ partial</span>
|
||
|
|
<span class="chip missing">○ missing</span>
|
||
|
|
</div>
|
||
|
|
</header>
|
||
|
|
|
||
|
|
<section class="category">
|
||
|
|
<div class="cat-head">
|
||
|
|
<span class="cat-num">01</span>
|
||
|
|
<h2>Core plumbing</h2>
|
||
|
|
</div>
|
||
|
|
<p class="cat-note">The mechanism Lunar core provides for offering and applying a shipping charge — everything else in this survey is built on top of it.</p>
|
||
|
|
|
||
|
|
<div class="feature">
|
||
|
|
<div class="f-name">Pluggable shipping option providers</div>
|
||
|
|
<span class="f-status have">have</span>
|
||
|
|
<div class="f-note"><code>Lunar\Base\ShippingModifier</code> abstract class + <code>ShippingManifest::addOption()</code> — any package can register options onto the manifest via a pipeline of modifiers (<code>ShippingModifiers::getModifiers()</code>).</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="feature">
|
||
|
|
<div class="f-name">Shipping applied to cart totals during calculate()</div>
|
||
|
|
<span class="f-status have">have</span>
|
||
|
|
<div class="f-note"><code>Lunar\Pipelines\Cart\ApplyShipping</code> — reads <code>ShippingManifest::getShippingOption($cart)</code> or a manual <code>shippingOptionOverride</code>, writes a <code>ShippingBreakdown</code> and <code>shippingSubTotal</code> onto the cart before <code>CalculateTax</code> runs.</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="feature">
|
||
|
|
<div class="f-name">Cart-level shippable check</div>
|
||
|
|
<span class="f-status have">have</span>
|
||
|
|
<div class="f-note"><code>Cart::isShippable()</code> — true if any line's <code>purchasable</code> (e.g. <code>ProductVariant::isShippable()</code>) is shippable; a digital-only cart skips the shipping-address requirement entirely.</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="feature">
|
||
|
|
<div class="f-name">Selecting a shipping option on the cart</div>
|
||
|
|
<span class="f-status have">have</span>
|
||
|
|
<div class="f-note"><code>Cart::setShippingOption()</code> → <code>SetShippingOption</code> action, validated by <code>ShippingOptionValidator</code>, triggers a recalculate. Nothing in 3dealer's storefront calls it yet — no shipping step exists in the UI.</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="feature">
|
||
|
|
<div class="f-name">Order-time shipping line snapshot</div>
|
||
|
|
<span class="f-status have">have</span>
|
||
|
|
<div class="f-note"><code>Lunar\Pipelines\Order\Creation\CreateShippingLine</code> writes an immutable <code>shipping</code>-type order line from the cart's shipping breakdown at checkout — survives later rate changes.</div>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section class="category">
|
||
|
|
<div class="cat-head">
|
||
|
|
<span class="cat-num">02</span>
|
||
|
|
<h2>Rate configuration (table-rate-shipping add-on)</h2>
|
||
|
|
</div>
|
||
|
|
<p class="cat-note"><code>lunarphp/table-rate-shipping</code> is installed (<code>composer.json</code>, pinned <code>^1.3</code>) and its <code>ShippingPlugin</code> is registered in <code>CorePlugin::boot()</code> — so 3dealer inherits it automatically, it doesn't need its own registration.</p>
|
||
|
|
|
||
|
|
<div class="feature">
|
||
|
|
<div class="f-name">Geographic shipping zones (country / state / postcode)</div>
|
||
|
|
<span class="f-status have">have</span>
|
||
|
|
<div class="f-note"><code>ShippingZone</code> model, type <code>unrestricted|countries|states|postcodes</code>; <code>ShippingZoneResolver::get()</code> matches a cart's address against zone scope, falling back to any <code>unrestricted</code> zone.</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="feature">
|
||
|
|
<div class="f-name">Flat-rate shipping</div>
|
||
|
|
<span class="f-status have">have</span>
|
||
|
|
<div class="f-note"><code>Drivers\ShippingMethods\FlatRate::resolve()</code> — one price per cart subtotal via <code>Pricing::for($shippingRate)</code>.</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="feature">
|
||
|
|
<div class="f-name">Weight- or total-tiered rates ("ship by")</div>
|
||
|
|
<span class="f-status have">have</span>
|
||
|
|
<div class="f-note"><code>Drivers\ShippingMethods\ShipBy::resolve()</code> — <code>data['charge_by']</code> is <code>cart_total</code> or <code>weight</code>, tiered via <code>priceBreaks</code>, with customer-group price overrides taking priority.</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="feature">
|
||
|
|
<div class="f-name">Free-shipping threshold</div>
|
||
|
|
<span class="f-status have">have</span>
|
||
|
|
<div class="f-note"><code>Drivers\ShippingMethods\FreeShipping::resolve()</code> — <code>data['minimum_spend']</code> (per-currency array supported), optional <code>use_discount_amount</code> to check against post-discount subtotal.</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="feature">
|
||
|
|
<div class="f-name">In-store pickup / collection</div>
|
||
|
|
<span class="f-status have">have</span>
|
||
|
|
<div class="f-note"><code>Drivers\ShippingMethods\Collection::resolve()</code> — zero-price option, flagged <code>collect: true</code> on the <code>ShippingOption</code>. Single implicit "store" — no concept of which location, no per-location stock or hours.</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="feature">
|
||
|
|
<div class="f-name">Per-product shipping exclusions by zone</div>
|
||
|
|
<span class="f-status have">have</span>
|
||
|
|
<div class="f-note"><code>ShippingExclusionList</code> + <code>ShippingZone::shippingExclusions()</code> — every driver checks it before resolving and returns <code>null</code> if any cart line's product is excluded from that zone.</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="feature">
|
||
|
|
<div class="f-name">Per-customer-group rate visibility</div>
|
||
|
|
<span class="f-status have">have</span>
|
||
|
|
<div class="f-note"><code>ShippingMethod::customerGroups()</code> pivot carries <code>visible</code>, <code>enabled</code>, <code>starts_at</code>, <code>ends_at</code> — scheduling and audience-gating a rate is already modeled.</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="feature">
|
||
|
|
<div class="f-name">Filament admin UI for zones/methods/rates</div>
|
||
|
|
<span class="f-status have">have</span>
|
||
|
|
<div class="f-note"><code>ShippingZoneResource</code>, <code>ShippingMethodResource</code>, <code>ShippingExclusionListResource</code> ship with the add-on — usable as soon as the Filament plugin is registered, which it is via <code>CorePlugin</code>.</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="feature">
|
||
|
|
<div class="f-name">Storefront checkout step to pick a rate</div>
|
||
|
|
<span class="f-status missing">missing</span>
|
||
|
|
<div class="f-note">No shipping views exist in 3dealer's <code>resources/views</code> beyond a passing mention in <code>components/footer.blade.php</code> — the whole backend above is unwired to any customer-facing UI.</div>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section class="category">
|
||
|
|
<div class="cat-head">
|
||
|
|
<span class="cat-num">03</span>
|
||
|
|
<h2>Carrier integration</h2>
|
||
|
|
</div>
|
||
|
|
<p class="cat-note">Real carriers quoting and printing on Lunar's behalf, rather than merchant-defined flat/tiered rates.</p>
|
||
|
|
|
||
|
|
<div class="feature">
|
||
|
|
<div class="f-name">Real-time carrier rate shopping (USPS/UPS/FedEx/DHL)</div>
|
||
|
|
<span class="f-status missing">missing</span>
|
||
|
|
<div class="f-note">No driver in <code>table-rate-shipping</code> calls an external carrier API — all four shipped drivers (<code>FlatRate</code>, <code>ShipBy</code>, <code>FreeShipping</code>, <code>Collection</code>) compute from local data. Shopify's <code>CarrierService</code> API is the model for this: shop sends weight/dims/destination, carrier returns live rates at checkout.</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="feature">
|
||
|
|
<div class="f-name">Product/variant weight & dimensions for rating</div>
|
||
|
|
<span class="f-status partial">partial</span>
|
||
|
|
<div class="f-note"><code>ProductVariant</code> has <code>weight_value</code>/<code>weight_unit</code> (referenced in <code>ShipBy</code>'s weight tier and <code>docs/lunar.md</code>) but no length/width/height fields exist in core migrations — enough for weight-tier rating, not enough for carrier-grade dimensional/volumetric quotes.</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="feature">
|
||
|
|
<div class="f-name">Shipping label generation & printing (staff-facing)</div>
|
||
|
|
<span class="f-status missing">missing</span>
|
||
|
|
<div class="f-note">No label concept anywhere in core or the add-on. Shopify has this built in for US merchants (USPS/UPS labels from admin or mobile); WooCommerce/PrestaShop lean on Shippo/EasyPost-style apps.</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="feature">
|
||
|
|
<div class="f-name">Return / exchange label generation</div>
|
||
|
|
<span class="f-status missing">missing</span>
|
||
|
|
<div class="f-note">No returns concept exists in Lunar core at all — this sits behind both "labels" and "returns," neither of which exists yet.</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="feature">
|
||
|
|
<div class="f-name">Shipment tracking numbers on orders</div>
|
||
|
|
<span class="f-status missing">missing</span>
|
||
|
|
<div class="f-note"><code>OrderShippingZone</code> pivot table records which zone an order matched, but no field anywhere stores a carrier tracking number or shipment status.</div>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section class="category">
|
||
|
|
<div class="cat-head">
|
||
|
|
<span class="cat-num">04</span>
|
||
|
|
<h2>Fulfillment logistics</h2>
|
||
|
|
</div>
|
||
|
|
<p class="cat-note">Where an order physically ships from, and whether it can ship from more than one place.</p>
|
||
|
|
|
||
|
|
<div class="feature">
|
||
|
|
<div class="f-name">Multi-warehouse / multi-location inventory</div>
|
||
|
|
<span class="f-status missing">missing</span>
|
||
|
|
<div class="f-note">No warehouse, location, or fulfillment-center model anywhere in <code>vendor/lunarphp/core</code> or <code>lunar</code> — stock is a flat quantity on the variant. WooCommerce needs Calcurates or WooCommerce Warehouses add-ons for this; it's genuinely not a Lunar concept at all.</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="feature">
|
||
|
|
<div class="f-name">Split shipment (one order, multiple packages/warehouses)</div>
|
||
|
|
<span class="f-status missing">missing</span>
|
||
|
|
<div class="f-note">Downstream of multi-warehouse — with a single implicit stock pool, there's nothing to split by. <code>CreateShippingLine</code> writes exactly one shipping line per order.</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="feature">
|
||
|
|
<div class="f-name">Multiple pickup locations (choose a specific store)</div>
|
||
|
|
<span class="f-status missing">missing</span>
|
||
|
|
<div class="f-note">The <code>Collection</code> driver models pickup as a single yes/no rate per zone — no location entity to pick from, no per-location hours/capacity.</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="feature">
|
||
|
|
<div class="f-name">Local delivery (distinct from carrier shipping or pickup)</div>
|
||
|
|
<span class="f-status missing">missing</span>
|
||
|
|
<div class="f-note">No radius/zone-based "we deliver it ourselves" driver — only <code>ShipBy</code>/<code>FlatRate</code> (carrier-agnostic priced shipping) and <code>Collection</code> (pickup) exist as concepts.</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="feature">
|
||
|
|
<div class="f-name">Delivery date / time-slot selection at checkout</div>
|
||
|
|
<span class="f-status missing">missing</span>
|
||
|
|
<div class="f-note">No date/time field on <code>ShippingOption</code>, <code>CartAddress</code>, or the order shipping line. WooCommerce needs a dedicated delivery-date-picker plugin for this too — not a gap unique to Lunar, but still open here.</div>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<section class="category">
|
||
|
|
<div class="cat-head">
|
||
|
|
<span class="cat-num">05</span>
|
||
|
|
<h2>International & risk</h2>
|
||
|
|
</div>
|
||
|
|
<p class="cat-note">What happens when a shipment crosses a border, or something goes wrong in transit.</p>
|
||
|
|
|
||
|
|
<div class="feature">
|
||
|
|
<div class="f-name">Customs documentation / HS codes per product</div>
|
||
|
|
<span class="f-status missing">missing</span>
|
||
|
|
<div class="f-note">No HS-code or customs-description field found on <code>Product</code>/<code>ProductVariant</code> migrations. Every international shipment needs one per line item to clear customs — researched requirement, not yet modeled anywhere in Lunar.</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="feature">
|
||
|
|
<div class="f-name">Duties/taxes collected at checkout (DDP)</div>
|
||
|
|
<span class="f-status missing">missing</span>
|
||
|
|
<div class="f-note">Lunar's <code>CalculateTax</code> pipeline step handles sales tax/VAT on the cart itself, not import duty estimation for cross-border orders. DDP vs. DDU is the standard framing (seller-collects-upfront vs. customer-pays-on-delivery) — neither is modeled.</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="feature">
|
||
|
|
<div class="f-name">Country/zone-restricted shipping</div>
|
||
|
|
<span class="f-status have">have</span>
|
||
|
|
<div class="f-note"><code>ShippingZone</code> type <code>countries</code>/<code>states</code>/<code>postcodes</code> already scopes which rates apply where — the building block international shipping would sit on top of.</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="feature">
|
||
|
|
<div class="f-name">Shipping insurance / package protection at checkout</div>
|
||
|
|
<span class="f-status missing">missing</span>
|
||
|
|
<div class="f-note">No insurance line-item concept in core. On Shopify this is exclusively third-party (ShipInsure, Route, Simply Shipping Protection) — not a platform-native feature there either, so the gap is normal, not distinctive.</div>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
<footer class="page">
|
||
|
|
<span>Compiled 2026-08-28 — inline citations from <code>vendor/lunarphp/core</code> and <code>vendor/lunarphp/table-rate-shipping</code> source are direct reads; DDP/DDU, carrier-API, label, and warehouse claims are sourced from web research on Shopify/WooCommerce/PrestaShop, marked accordingly by context.</span>
|
||
|
|
<span>boboko-core / docs</span>
|
||
|
|
</footer>
|
||
|
|
|
||
|
|
</div>
|