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)