# -*- coding: utf-8 -*- """Landed-cost calculator for a Greek online order. Published with "What a Greek online order actually costs" so the model in that paper can be run rather than read. Pure Python, no dependencies. python landed-cost-calculator.py # the worked examples python landed-cost-calculator.py --value 45 --items 3 --payment cod --channel marketplace EVERY RATE CARRIES ITS SOURCE AND RETRIEVAL DATE. Where a figure is not published anywhere, the calculator does not invent one: it returns None for that component and says so in the output. A total containing an unpriced component is reported as a floor, never as a total. """ import argparse RETRIEVED = '2026-08-07' # ── published rates ───────────────────────────────────────────────────────── RATES = { 'duty_per_item_eur': (3.00, 'Council Regulation (EU) 2026/382: flat EUR 3 customs duty on distance ' 'sales up to EUR 150, per ITEM not per consignment, from 1 July 2026 ' 'until 1 July 2028', '2026-08-06'), 'duty_threshold_eur': (150.00, 'Same regulation: applies to consignments up to EUR 150', '2026-08-06'), 'vat_standard': (0.24, 'Greek standard VAT rate', '2026-08-06'), 'card_online_pct': (0.0219, 'Viva published online card rate', RETRIEVED), 'card_online_fixed': (0.24, 'Viva published online card fixed component', RETRIEVED), 'card_capped_pct': (0.005, 'Statutory cap, Law 5167/2024 Article 50: domestic consumer cards on ' 'transactions up to EUR 20', RETRIEVED), 'card_cap_ceiling': (20.00, 'Ceiling for the statutory cap', RETRIEVED), 'locker_domestic': (3.00, 'BOX NOW published from-price, domestic', RETRIEVED), 'locker_cyprus': (5.00, 'BOX NOW published from-price, Cyprus', RETRIEVED), 'locker_eu': (15.00, 'BOX NOW published from-price, most EU destinations', RETRIEVED), 'courier_same_city_p2d': (5.70, 'ACS tariff v7.5, August 2026, point-to-door, to 2 kg', RETRIEVED), 'courier_mainland_p2d': (11.20, 'ACS tariff v7.5, August 2026, point-to-door, to 2 kg', RETRIEVED), 'courier_islands_p2d': (11.90, 'ACS tariff v7.5, August 2026, point-to-door, to 2 kg', RETRIEVED), } # ── components nobody publishes ───────────────────────────────────────────── UNPRICED = { 'marketplace_commission': 'The dominant Greek marketplace returns HTTP 403 to an identified research client and ' 'publishes no rate card reachable from a public page. For a shop selling through it this ' 'is probably the largest single per-order line.', 'cod_fee': 'Cash-on-delivery handling is set in a courier contract, and no incumbent publishes ' 'contract terms.', 'returns_provision': 'A function of return rate multiplied by handling cost. The return rate is merchant ' 'operational data, not a published figure.', } def money(x): return ' - ' if x is None else '%7.2f' % x def landed_cost(value_eur, items=1, payment='card', destination='domestic', delivery='locker', channel='own-site', imported=False, domestic_consumer_card=True): """Return (lines, floor_total, unpriced) for one order. `floor_total` is a FLOOR whenever `unpriced` is non-empty. """ lines, unpriced = [], [] # customs duty, per item, only on imported low-value consignments if imported: if value_eur <= RATES['duty_threshold_eur'][0]: duty = RATES['duty_per_item_eur'][0] * items lines.append(('customs duty (EUR 3 x %d items)' % items, duty, RATES['duty_per_item_eur'][1])) else: lines.append(('customs duty', None, 'Above the EUR 150 threshold the flat duty does not apply; the ordinary ' 'tariff does, and it is commodity-specific')) unpriced.append('customs duty above EUR 150') # card processing if payment == 'card': if domestic_consumer_card and value_eur <= RATES['card_cap_ceiling'][0]: fee = value_eur * RATES['card_capped_pct'][0] lines.append(('card processing (statutory 0.50%)', fee, RATES['card_capped_pct'][1])) else: fee = value_eur * RATES['card_online_pct'][0] + RATES['card_online_fixed'][0] lines.append(('card processing (2.19% + EUR 0.24)', fee, RATES['card_online_pct'][1])) elif payment == 'cod': lines.append(('cash-on-delivery handling', None, UNPRICED['cod_fee'])) unpriced.append('cash-on-delivery handling') # delivery key = {('locker', 'domestic'): 'locker_domestic', ('locker', 'cyprus'): 'locker_cyprus', ('locker', 'eu'): 'locker_eu', ('courier', 'domestic'): 'courier_same_city_p2d', ('courier', 'mainland'): 'courier_mainland_p2d', ('courier', 'islands'): 'courier_islands_p2d'}.get((delivery, destination)) if key: lines.append(('delivery (%s, %s)' % (delivery, destination), RATES[key][0], RATES[key][1])) else: lines.append(('delivery', None, 'No published from-price for that combination')) unpriced.append('delivery') # marketplace commission if channel == 'marketplace': lines.append(('marketplace commission', None, UNPRICED['marketplace_commission'])) unpriced.append('marketplace commission') # returns lines.append(('returns provision', None, UNPRICED['returns_provision'])) unpriced.append('returns provision') floor = sum(v for _, v, _ in lines if v is not None) return lines, floor, unpriced def show(title, **kw): value = kw.get('value_eur') lines, floor, unpriced = landed_cost(**kw) print() print('=' * 78) print('%s - order value EUR %.2f' % (title, value)) print('=' * 78) for label, v, src in lines: print(' %-42s %s' % (label, money(v))) print(' %-42s %s' % ('-' * 42, '-------')) verdict = 'FLOOR (incomplete)' if unpriced else 'TOTAL' print(' %-42s %s <- %s' % (verdict, money(floor), verdict)) print(' %-42s %6.1f%% of order value' % ('as a share of the order', 100.0 * floor / value)) if unpriced: print() print(' NOT PRICED, and therefore NOT in the number above:') for u in unpriced: print(' - %s' % u) print(' The figure above is a FLOOR. The real cost is higher by an amount') print(' this calculator refuses to guess.') def main(): ap = argparse.ArgumentParser() ap.add_argument('--value', type=float) ap.add_argument('--items', type=int, default=1) ap.add_argument('--payment', choices=['card', 'cod'], default='card') ap.add_argument('--destination', default='domestic') ap.add_argument('--delivery', choices=['locker', 'courier'], default='locker') ap.add_argument('--channel', choices=['own-site', 'marketplace'], default='own-site') ap.add_argument('--imported', action='store_true') a = ap.parse_args() if a.value: show('Your order', value_eur=a.value, items=a.items, payment=a.payment, destination=a.destination, delivery=a.delivery, channel=a.channel, imported=a.imported) return print('LANDED-COST CALCULATOR - published rates as at %s' % RETRIEVED) print('Run with --value to price your own order. --help lists the options.') show('Small domestic order, card, locker', value_eur=15.0, items=1) show('Same order paid cash on delivery', value_eur=15.0, items=1, payment='cod') show('Domestic order, card, courier to the mainland', value_eur=45.0, items=2, delivery='courier', destination='mainland') show('Sold through the marketplace', value_eur=45.0, items=2, channel='marketplace') show('Imported low-value basket, six items', value_eur=60.0, items=6, imported=True) show('Cross-border order to the EU', value_eur=60.0, items=1, destination='eu') print() print('=' * 78) print('SOURCES') print('=' * 78) for k, (v, src, when) in sorted(RATES.items()): print(' %-24s %8.4f %s [%s]' % (k, v, src[:66], when)) if __name__ == '__main__': main()