PostgreSQL sometimes fails to detect serialization failures
Doing this in a serialized transaction should work, unless there is a bug.
As a workaround, use this command, which tries to add a new license; if the license key already exists, it adds the new hours to the existing value:
INSERT INTO licenses(license_key, product_id, hours)
VALUES ($1,
        (SELECT product_id FROM products WHERE product_code = $2 LIMIT 1),
        $3)
ON CONFLICT (license_key) DO UPDATE
SET hours = hours + EXCLUDED.hours
RETURNING id;

documentation