Best database design for payments with multiple constraints
You obviously put some thought into this design. But you would regret that you have to search for codes in two tables. And what if you want to add details for codes?

I suggest to store all codes in the same table like this:

CREATE TABLE payment (
   idpayment serial PRIMARY KEY
   -- more payment details?
);

CREATE TABLE payment_code (
   idpayment_code serial PRIMARY KEY
 , idpayment      int NOT NULL REFERENCES payment
 , idthingC       int REFERENCES thingC  -- can be NULL
 , idthingD       int REFERENCES thingD  -- can be NULL
 , c1             int NOT NULL
 , c2             int NOT NULL
 , c3             int NOT NULL
 , CONSTRAINT either_c_or_d              -- reference either to c or to d
      CHECK (idthingC IS NULL AND idthingD IS NOT NULL
          OR idthingD IS NULL AND idthingC IS NOT NULL)
 , CONSTRAINT c_distinct_per_payment     -- distinct idthingC per payment (?)
      UNIQUE (idpayment, idthingC)
 );

CREATE UNIQUE INDEX idx_one_d_per_idpayment ON payment_code (idpayment)
WHERE idthingD IS NOT NULL;              -- only one idthingD per payment


(Depending on the nature of the codes, maybe even just 1 code per row.)

The partial unique index idx_one_d_per_idpayment disallows more than one reference to thingD per payment.

This allows at most 1 thingD per payment and any number of distinct thingC.

Mutually exclusive payment types

You later clarified, those are mutually exclusive.
This allows at most 1 thingD per payment or any number of distinct thingC (but not both):

CREATE TABLE payment (
   idpayment serial PRIMARY KEY
 , type_cd   "char" NOT NULL DEFAULT 'C'  -- optional default
   -- more payment details?
, CONSTRAINT type_cd_valid CHECK (type_cd = 'C' OR type_cd = 'D')
, CONSTRAINT redundant_uni_for_fk_constraint UNIQUE (idpayment, type_cd)
);

CREATE TABLE payment_code (
   idpayment_code serial PRIMARY KEY
 , idpayment      int NOT NULL      -- multicolumn ...
 , type_cd        "char" NOT NULL   -- ... FK reference
 , idthingC       int REFERENCES thingC
 , idthingD       int REFERENCES thingD
 , c1             int NOT NULL
 , c2             int NOT NULL
 , c3             int NOT NULL
 , CONSTRAINT either_c_or_d
      CHECK (type_cd = 'C' AND idthingC IS NOT NULL
          OR type_cd = 'D' AND idthingD IS NOT NULL)
 , CONSTRAINT c_distinct_per_payment
      UNIQUE (idpayment, idthingC)
 , CONSTRAINT payment_c_or_d
      FOREIGN KEY (idpayment, type_cd) REFERENCES payment(idpayment, type_cd)
 );

CREATE UNIQUE INDEX idx_one_d_per_idpayment ON payment_code (idpayment)
WHERE type_cd = 'D';  -- slightly simpler now


To enforce your rules, add a type column in table payment (I named it type_cd since "type" is too generic, and I chose data type "char" - with double quotes - efficient for tiny enumerations). Include that column in the FK reference (redundantly). This allows a CHECK constraint (either_c_or_d) in table payment_code to enforce valid references.

The UNIQUE constraint redundant_uni_for_fk_constraint seems redundant, but is required for the multicolumn FK reference on (idpayment, type_cd). Related, with more explanation:


Enforcing constraints &quot;two tables away&quot;


Both FK columns (idpayment, type_cd) must be NOT NULL to enforce referential integrity. Or (if you need rows without assigned payment) declare the FK constraint as MATCH FULL. Details:


Two-column foreign key constraint only when third column is NOT NULL
MATCH FULL vs MATCH SIMPLE in foreign key constraints