Initial commit.

This commit is contained in:
Edward Betts 2026-09-19 11:32:53 +01:00
commit 628e5c3823
26 changed files with 3478 additions and 0 deletions

118
tests/test_receipt.py Normal file
View file

@ -0,0 +1,118 @@
from copy import deepcopy
from decimal import Decimal
import json
from pathlib import Path
import pytest
from ocado_grocy.receipt import ImportError, number, parse_document, parse_ocado_order
FIXTURE = Path(__file__).parent / 'fixtures' / 'history_receipt.json'
def api_data():
return json.loads(FIXTURE.read_text())
def test_synthetic_api_receipt():
receipt = parse_ocado_order(api_data())
assert receipt.order_id == '1000000000001'
assert receipt.purchased_date == '2020-01-02'
assert len(receipt.items) == 21
assert sum(i.quantity for i in receipt.items) == 23
assert sum(i.total for i in receipt.items) == Decimal('28.75')
lettuce = next(i for i in receipt.items if 'Lettuce' in i.name)
assert lettuce.best_before == '2020-01-05'
assert lettuce.metadata['storage_type'] == 'FRIDGE'
cloths = next(i for i in receipt.items if 'Cleaning Cloths' in i.name)
assert cloths.quantity == 2
assert cloths.unit_price == Decimal('1.25')
assert cloths.best_before is None
def test_order_identity_and_delivery_status():
with pytest.raises(ImportError,match='does not match'):
parse_ocado_order(api_data(),expected_order_id='123')
data=api_data()
data['entities']['order'][data['result']]['status']='CONFIRMED'
with pytest.raises(ImportError,match='not delivered'):
parse_ocado_order(data)
def test_summary_detects_partial_receipt():
data=api_data()
data['entities']['order'][data['result']]['groupedProducts']['products'].pop()
with pytest.raises(ImportError,match='quantities'):
parse_ocado_order(data)
def test_summary_detects_wrong_price():
data=api_data()
data['entities']['order'][data['result']]['groupedProducts']['products'][0]['prices']['offered']['amount']='9.99'
with pytest.raises(ImportError,match='prices'):
parse_ocado_order(data)
def test_only_accepted_substitutes_are_imported():
data=api_data()
order=data['entities']['order'][data['result']]
original=order['groupedProducts']['products'].pop(0)
accepted={**deepcopy(original),'productId':'replacement','retailerProductId':'999','name':'Replacement bananas','status':'ACCEPTED'}
rejected={**deepcopy(original),'productId':'rejected','retailerProductId':'888','name':'Rejected bananas','status':'REJECTED'}
original['substitutes']=[accepted,rejected]
order['groupedProducts']['substitutes']=[original]
receipt=parse_ocado_order(data)
assert len(receipt.items)==21
assert any(i.product_id=='999' for i in receipt.items)
assert not any(i.product_id in {'888',original['retailerProductId']} for i in receipt.items)
assert next(i for i in receipt.items if i.product_id=='999').metadata['substituted_for']==original['name']
def test_input_order_does_not_change_stable_line_order():
first=api_data()
second=deepcopy(first)
second['entities']['order'][second['result']]['groupedProducts']['products'].reverse()
assert parse_ocado_order(first)==parse_ocado_order(second)
def test_discontinued_product_does_not_need_html_link():
data=api_data()
product=data['entities']['order'][data['result']]['groupedProducts']['products'][0]
product['isInCurrentCatalog']=False
receipt=parse_ocado_order(data)
assert any(i.product_id==product['retailerProductId'] for i in receipt.items)
def test_purchase_date_is_local_delivery_date():
data=api_data()
data['entities']['order'][data['result']]['dates']['deliveryStartDate']='2026-09-01T23:30:00Z'
assert parse_ocado_order(data).purchased_date=='2026-09-02'
@pytest.mark.parametrize('value',['NaN','Infinity','-1','nonsense',None])
def test_invalid_numbers(value):
with pytest.raises(ImportError):number(value,'quantity')
def test_free_and_weighted_items():
receipt=parse_document({'order_id':'1','items':[
{'name':'Free gift','quantity':1,'total':0},
{'name':'Apples','quantity':'0.75','total':'1.50'}]})
assert receipt.items[0].unit_price==0
assert receipt.items[1].unit_price==2
def test_receipt_json_roundtrip():
receipt=parse_ocado_order(api_data())
assert parse_document(json.loads(json.dumps(receipt.export(),default=str)))==receipt
def test_live_basket_cannot_be_mistaken_for_order():
with pytest.raises(ImportError,match='structure'):
parse_ocado_order({'data':{'basket':{'items':[{'name':'Not an order'}]}}})
def test_non_gbp_order_rejected():
data=api_data()
data['entities']['order'][data['result']]['groupedProducts']['products'][0]['prices']['offered']['currency']='EUR'
with pytest.raises(ImportError,match='currency'):parse_ocado_order(data)