A South African men's apparel store on Spree 5.6 + Next.js 15. 160 products
live, three markets, live Malaysian courier rates, customer-owned carts.
What is here:
storefront/ the Next.js app
backend/ ONLY the custom Spree code. Spree itself is a prebuilt image
that mounts no app code, so every file here needs an explicit
volume entry in docker-compose.yml or it is silently ignored.
catalogue/ the scripts that loaded and maintain the products
docs/ manifest, mock registry, napkins
Real .env files are committed deliberately. This lives on the house's private
Gitea, and a clone that runs beats a clone that needs a secrets handover.
LANDMINE.md maps every key, what it opens, and what must be rotated before this
repo goes anywhere less private.
The admin API key used to be a literal in six catalogue scripts. They now read
SPREE_ADMIN_KEY from the environment and refuse to run without it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
59 lines
2.5 KiB
Ruby
59 lines
2.5 KiB
Ruby
# EasyParcel live courier rates as a real Spree shipping cost.
|
|
#
|
|
# The storefront has always shown live EasyParcel quotes — 20 couriers for a
|
|
# Melaka address, with logos and durations. What was missing is that the chosen
|
|
# one never reached Spree, so the customer picked "City-Link RM6.79" and the
|
|
# order was charged whatever the flat-rate method said. The quote screen looked
|
|
# decorative because of that last missing step, and I removed it on that reading.
|
|
# This restores it by making the selection the thing Spree actually charges.
|
|
#
|
|
# The amount is read from order.private_metadata['easyparcel_amount'], written
|
|
# ONLY by the storefront's /api/shipping/select route, which re-fetches the live
|
|
# quotes server-side and confirms the chosen service_id really costs that. The
|
|
# browser can PATCH cart metadata directly, so an unverified amount here would
|
|
# let a customer set their own shipping to one sen.
|
|
#
|
|
# No quote chosen => nil => no shipping rate => no fulfillment => checkout cannot
|
|
# complete. That is deliberate: a Malaysian order must carry a real courier.
|
|
#
|
|
# The class is defined INSIDE to_prepare: at initializer load time Spree is not
|
|
# autoloaded yet, and `require 'spree/shipping_calculator'` raises LoadError
|
|
# under Zeitwerk — that took the container down once.
|
|
#
|
|
# Bind-mounted — the prebuilt image mounts no app code. See docker-compose.yml.
|
|
|
|
Rails.application.config.to_prepare do
|
|
unless defined?(Spree::Calculator::Shipping::EasyParcelQuote)
|
|
Spree::Calculator::Shipping.const_set(
|
|
:EasyParcelQuote,
|
|
Class.new(Spree::ShippingCalculator) do
|
|
preference :currency, :string, default: 'MYR'
|
|
|
|
def self.description
|
|
'EasyParcel live courier quote'
|
|
end
|
|
|
|
def compute_package(package)
|
|
order = package.respond_to?(:order) ? package.order : nil
|
|
return nil if order.nil?
|
|
|
|
meta = order.private_metadata || {}
|
|
raw = meta['easyparcel_amount'] || meta[:easyparcel_amount]
|
|
return nil if raw.blank?
|
|
|
|
BigDecimal(raw.to_s)
|
|
rescue ArgumentError, TypeError
|
|
# A malformed amount must not become a free shipment.
|
|
nil
|
|
end
|
|
end
|
|
)
|
|
end
|
|
|
|
# Registering only populates the admin's calculator dropdown; the method below
|
|
# gets the class assigned directly, so a nil list here must not abort boot.
|
|
klass = Spree::Calculator::Shipping::EasyParcelQuote
|
|
list = Rails.application.config.spree.calculators&.shipping_methods
|
|
list << klass if list.respond_to?(:include?) && !list.include?(klass)
|
|
end
|