Files
juxtapoandClaude Opus 5 a58a61b98c Pak Jabit: full stack — storefront, Spree customisations, catalogue tooling
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>
2026-09-15 22:09:55 +08:00

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