For each implementation in joins
To get the full domain you need a cartesian product (cross join in sql lingo) between the two sets:

select distinct d.customer_id, i.item_id 
from deliveries d 
cross join items i;


from this set you can subtract the actual deliveries:

select distinct d.customer_id, i.item_id 
from deliveries d 
cross join items i 
where not exists (
    select 1 from deliveries d2 
    where i.item_id = d2.item_id 
    and d.customer_id = d2.customer_id
);


I'm not clear on how this null comes into play, but you can add a flag that says that this product has not been bought by this customer as:

select distinct d.customer_id, i.item_id, 'NOT BOUGHT' as status 
from deliveries d 
cross join items i 
where not exists (
    select 1 from deliveries d2 
    where i.item_id = d2.item_id 
    and d.customer_id = d2.customer_id
);


Finally, you can add the deliveries table to this set as:

select distinct d.customer_id, i.item_id, 'NOT BOUGHT' as status 
from deliveries d 
cross join items i 
where not exists (
    select 1 from deliveries d2 
    where i.item_id = d2.item_id 
    and d.customer_id = d2.customer_id
);
union all 
select d.customer_id, d.item_id, 'BOUGHT' as status 
from deliveries d;


Not sure if it answers your question, but I think it will give you a start.