Skip to content

Landed-cost model

Status: STUB. This doc names the surfaces and the inputs; the model itself isn't shipped yet. M2 candidate.

What "landed cost" means here

The total a Lebanese buyer pays to take a part home, not just the catalog price. Three components:

  1. Catalog price — what the retailer's website displays (USD only per ADR-0011).
  2. Delivery / shipping — varies by retailer, by district, by basket weight.
  3. VAT — 11% standard rate (12% pending Parliamentary enactment per #163). Some retailers display VAT-included; some VAT-excluded; we need to know which.
  4. Retailer surcharges — cash-on-delivery fees, fresh-USD vs lollar bifurcation, BANCO conversions for non-Beirut delivery.

Why 961tech needs to model it

Without landed cost, the cheapest catalog price is misleading. A buyer who picks Retailer A at $399 may actually pay more than Retailer B at $410 once VAT-included flag, $5 delivery, and a 1% cash-handling surcharge land. Persona research flags this as the #2 buyer frustration after stock uncertainty.

Per-retailer fields needed (M2 schema delta)

model Retailer {
  // existing fields...
  vatHandling        String?  // 'inclusive' | 'exclusive' | 'unknown'
  deliveryPolicy     Json?    // { freeOver: number, baseFee: number, perKgFee: number, beyondBeirut: number }
  paymentSurcharges  Json?    // { cashOnDelivery: 0.01, wishMoney: 0, omt: 0.005, cardFreshUsd: 0, cardLollar: 0.15 }
}

deliveryPolicy and paymentSurcharges modelled as JSON to start — formal columns once the shape stabilises.

Per-product fields

Mostly already there: - weightKg — needed for per-kg delivery fees. Currently absent on Product; populate via #21 LLM extraction or scraper-side enrichment. - dimensionsCmCm — bulky-item surcharges (some retailers).

Modelling approach

interface LandedCostInput {
  retailer: Retailer;
  basketTotalUsd: number;
  basketKg: number;
  deliveryDistrict: 'beirut' | 'mt-lebanon' | 'north' | 'south' | 'beqaa' | 'nabatieh' | 'unknown';
  paymentMethod: 'card-fresh' | 'card-lollar' | 'cash-cod' | 'wish' | 'omt' | 'unknown';
}

interface LandedCostBreakdown {
  catalogUsd: number;
  vatUsd: number;          // 0 if VAT inclusive
  deliveryUsd: number;
  paymentSurchargeUsd: number;
  totalUsd: number;
  notes: string[];         // surfaced as caveats next to the number
}

export function computeLandedCost(input: LandedCostInput): LandedCostBreakdown;

Each field has a fallback if the data isn't filled in: - VAT inclusive vs exclusive defaults to "inclusive" (Lebanese retailer convention) - Delivery defaults to retailer's published "delivery within Beirut" rate - Payment surcharge defaults to 0 (we don't penalise payment-method ambiguity)

The breakdown returns notes so the UI can surface "VAT handling unknown — assumed inclusive" rather than silently misleading.

UI surfaces (M2)

  • Product detail card: cheapest landed cost vs cheapest catalog price, with the diff as a small chip.
  • Build summary: total landed cost across retailers (combined with the M2 cart-aggregator #15).
  • Retailer profile: documented payment + delivery policy for transparency.
  • Per-card freshness signal (page-content-spec.md) gains a landed-cost annotation when the buyer-side delta is > 5%.

What we're explicitly NOT doing

  • Currency conversion display. ADR-0004 is English+USD only; no LBP / EUR / SAR projection.
  • Customs / import duties. All catalog-listed parts are post-import; the retailer absorbs that cost.
  • Real-time exchange rates. We're not a forex aggregator.
  • Negotiated retailer discounts. If MASTER strikes a retailer-specific buyer discount via affiliate, that's per-retailer per-tier and lives in #16 admin tools.

See also