merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

merchant-0036-init.sql.fragment (81615B)


      1 --
      2 -- This file is part of TALER
      3 -- Copyright (C) 2026 Taler Systems SA
      4 --
      5 -- TALER is free software; you can redistribute it and/or modify it under the
      6 -- terms of the GNU General Public License as published by the Free Software
      7 -- Foundation; either version 3, or (at your option) any later version.
      8 --
      9 -- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
     10 -- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11 -- A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
     12 --
     13 -- You should have received a copy of the GNU General Public License along with
     14 -- TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 
     16 -- @file merchant-0036-init.sql.fragment
     17 -- @brief Create initial set of per-instance tables and indices
     18 -- @author Christian Grothoff
     19 
     20 CREATE PROCEDURE merchant.merchant_0036_init(s TEXT)
     21   LANGUAGE plpgsql
     22   AS $OUTER$
     23 BEGIN
     24   -- All object creation below uses unqualified names that resolve against
     25   -- the per-instance schema `s`.  Cross-schema references (the merchant.*
     26   -- types, the global merchant.* tables and the public.* operator classes)
     27   -- stay explicitly qualified.  Tables are created in FK-dependency order so
     28   -- that foreign keys can be declared inline with the CREATE TABLE.
     29   EXECUTE format('SET LOCAL search_path TO %I', s);
     30 
     31   -- ===================================================================
     32   -- Tier 0: tables without intra-schema foreign keys.
     33   -- ===================================================================
     34 
     35   CREATE TABLE merchant_accounts (
     36     account_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
     37     h_wire BYTEA NOT NULL UNIQUE,
     38     salt BYTEA NOT NULL,
     39     credit_facade_url TEXT,
     40     credit_facade_credentials JSONB,
     41     last_bank_serial INT8 DEFAULT 0 NOT NULL,
     42     payto_uri TEXT NOT NULL UNIQUE,
     43     active BOOLEAN NOT NULL,
     44     extra_wire_subject_metadata TEXT,
     45     CONSTRAINT merchant_accounts_h_wire_check CHECK ((LENGTH(h_wire) = 64)),
     46     CONSTRAINT merchant_accounts_salt_check CHECK ((LENGTH(salt) = 16))
     47   );
     48   COMMENT ON TABLE merchant_accounts IS 'bank accounts of the instances';
     49   COMMENT ON COLUMN merchant_accounts.account_serial IS 'Unique identifier for this account, used to reference this account from other tables';
     50   COMMENT ON COLUMN merchant_accounts.h_wire IS 'salted hash of payto_uri';
     51   COMMENT ON COLUMN merchant_accounts.salt IS 'salt used when hashing payto_uri into h_wire';
     52   COMMENT ON COLUMN merchant_accounts.credit_facade_url IS 'Base URL of a facade where the merchant can inquire about incoming bank transactions into this account';
     53   COMMENT ON COLUMN merchant_accounts.credit_facade_credentials IS 'JSON with credentials needed to access the credit facade';
     54   COMMENT ON COLUMN merchant_accounts.last_bank_serial IS 'Serial number of the bank of the last transaction we successfully imported';
     55   COMMENT ON COLUMN merchant_accounts.payto_uri IS 'payto URI of a merchant bank account';
     56   COMMENT ON COLUMN merchant_accounts.active IS 'true if we actively use this bank account, false if it is just kept around for older contracts to refer to';
     57   COMMENT ON COLUMN merchant_accounts.extra_wire_subject_metadata IS 'Additional meta data that the exchange should include in wire transfer subjects made to this account on behalf of this instance';
     58 
     59   CREATE TABLE merchant_categories (
     60     category_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
     61     category_name TEXT NOT NULL UNIQUE,
     62     category_name_i18n JSONB NOT NULL
     63   );
     64   COMMENT ON TABLE merchant_categories IS 'product categories (with translations) to group products from inventory (primarily for the point-of-sale app)';
     65   COMMENT ON COLUMN merchant_categories.category_name IS 'name of the category';
     66   COMMENT ON COLUMN merchant_categories.category_name_i18n IS 'JSON with translations of the category name';
     67   CREATE INDEX trgm_idx_categories_by_name
     68     ON merchant_categories USING gin (lower(category_name) public.gin_trgm_ops);
     69 
     70   CREATE TABLE merchant_contract_terms (
     71     order_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
     72     order_id TEXT NOT NULL UNIQUE,
     73     contract_terms JSONB NOT NULL,
     74     wallet_data TEXT,
     75     h_contract_terms BYTEA NOT NULL UNIQUE,
     76     creation_time INT8 NOT NULL,
     77     pay_deadline INT8 NOT NULL,
     78     refund_deadline INT8 NOT NULL,
     79     paid BOOLEAN DEFAULT FALSE NOT NULL,
     80     wired BOOLEAN DEFAULT FALSE NOT NULL,
     81     fulfillment_url TEXT,
     82     session_id TEXT DEFAULT ''::text NOT NULL,
     83     pos_key TEXT,
     84     pos_algorithm INT4 DEFAULT 0 NOT NULL,
     85     claim_token BYTEA NOT NULL,
     86     choice_index INT2,
     87     CONSTRAINT merchant_contract_terms_claim_token_check CHECK ((LENGTH(claim_token) = 16)),
     88     CONSTRAINT merchant_contract_terms_h_contract_terms_check CHECK ((LENGTH(h_contract_terms) = 64))
     89   );
     90   COMMENT ON TABLE merchant_contract_terms IS 'Contracts are orders that have been claimed by a wallet';
     91   COMMENT ON COLUMN merchant_contract_terms.order_id IS 'Not a foreign key into merchant_orders because paid contracts persist after expiration';
     92   COMMENT ON COLUMN merchant_contract_terms.contract_terms IS 'These contract terms include the wallet nonce';
     93   COMMENT ON COLUMN merchant_contract_terms.wallet_data IS 'Data provided by the wallet when paying for the contract (subcontract selection, blinded tokens, etc.)';
     94   COMMENT ON COLUMN merchant_contract_terms.h_contract_terms IS 'Hash over contract_terms';
     95   COMMENT ON COLUMN merchant_contract_terms.creation_time IS 'time at which the order was originally created';
     96   COMMENT ON COLUMN merchant_contract_terms.pay_deadline IS 'How long is the offer valid. After this time, the order can be garbage collected';
     97   COMMENT ON COLUMN merchant_contract_terms.refund_deadline IS 'By what times do refunds have to be approved (useful to reject refund requests)';
     98   COMMENT ON COLUMN merchant_contract_terms.paid IS 'true implies the customer paid for this contract; order should be DELETEd from merchant_orders once paid is set to release merchant_order_locks; paid remains true even if the payment was later refunded';
     99   COMMENT ON COLUMN merchant_contract_terms.wired IS 'true implies the exchange wired us the full amount for all non-refunded payments under this contract';
    100   COMMENT ON COLUMN merchant_contract_terms.fulfillment_url IS 'also included in contract_terms, but we need it here to SELECT on it during repurchase detection; can be NULL if the contract has no fulfillment URL';
    101   COMMENT ON COLUMN merchant_contract_terms.session_id IS 'last session_id from we confirmed the paying client to use, empty string for none';
    102   COMMENT ON COLUMN merchant_contract_terms.pos_key IS 'enconded based key which is used for the verification of payment';
    103   COMMENT ON COLUMN merchant_contract_terms.pos_algorithm IS 'specifies the algorithm used to compute the verification code (usually some OTP variant)';
    104   COMMENT ON COLUMN merchant_contract_terms.claim_token IS 'Token optionally used to access the status of the order. All zeros (not NULL) if not used';
    105   COMMENT ON COLUMN merchant_contract_terms.choice_index IS 'Index of selected choice. Refers to the `choices` array in the contract terms. NULL for contracts without choices.';
    106   CREATE INDEX merchant_contract_terms_by_expiration
    107     ON merchant_contract_terms (paid, pay_deadline);
    108   COMMENT ON INDEX merchant_contract_terms_by_expiration IS 'for unlock_contracts';
    109   CREATE INDEX merchant_contract_terms_by_merchant_and_expiration
    110     ON merchant_contract_terms (pay_deadline);
    111   COMMENT ON INDEX merchant_contract_terms_by_merchant_and_expiration IS 'for delete_contract_terms';
    112   CREATE INDEX merchant_contract_terms_by_merchant_and_payment
    113     ON merchant_contract_terms (paid);
    114   CREATE INDEX merchant_contract_terms_by_merchant_and_session
    115     ON merchant_contract_terms (session_id);
    116   CREATE INDEX merchant_contract_terms_by_merchant_session_and_fulfillment
    117     ON merchant_contract_terms (fulfillment_url, session_id);
    118   CREATE INDEX trgm_idx_contract_summaries
    119     ON merchant_contract_terms USING gin (lower((contract_terms ->> 'summary'::text)) public.gin_trgm_ops);
    120 
    121   CREATE TABLE merchant_custom_units (
    122     unit_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    123     unit TEXT NOT NULL UNIQUE,
    124     unit_name_long TEXT NOT NULL,
    125     unit_name_short TEXT NOT NULL,
    126     unit_name_long_i18n BYTEA DEFAULT convert_to('{}'::text, 'UTF8'::name) NOT NULL,
    127     unit_name_short_i18n BYTEA DEFAULT convert_to('{}'::text, 'UTF8'::name) NOT NULL,
    128     unit_allow_fraction BOOLEAN DEFAULT FALSE NOT NULL,
    129     unit_precision_level INT4 DEFAULT 0 NOT NULL,
    130     unit_active BOOLEAN DEFAULT TRUE NOT NULL,
    131     CONSTRAINT merchant_custom_units_unit_precision_level_check
    132       CHECK (((unit_precision_level >= 0) AND (unit_precision_level <= 6)))
    133   );
    134   COMMENT ON TABLE merchant_custom_units IS 'Per-instance custom measurement units.';
    135   COMMENT ON COLUMN merchant_custom_units.unit IS 'FIXME';
    136   COMMENT ON COLUMN merchant_custom_units.unit_name_long IS 'FIXME';
    137   COMMENT ON COLUMN merchant_custom_units.unit_name_short IS 'FIXME';
    138   COMMENT ON COLUMN merchant_custom_units.unit_name_long_i18n IS 'FIXME';
    139   COMMENT ON COLUMN merchant_custom_units.unit_name_short_i18n IS 'FIXME';
    140   COMMENT ON COLUMN merchant_custom_units.unit_allow_fraction IS 'FIXME';
    141   COMMENT ON COLUMN merchant_custom_units.unit_precision_level IS 'FIXME';
    142   COMMENT ON COLUMN merchant_custom_units.unit_active IS 'FIXME';
    143 
    144   CREATE TABLE merchant_donau_instances (
    145     donau_instances_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    146     donau_url TEXT NOT NULL,
    147     charity_name TEXT NOT NULL,
    148     charity_id INT8 NOT NULL,
    149     charity_max_per_year merchant.taler_amount_currency NOT NULL,
    150     charity_receipts_to_date merchant.taler_amount_currency NOT NULL,
    151     current_year INT8 NOT NULL,
    152     UNIQUE (donau_url, charity_id)
    153   );
    154   COMMENT ON TABLE merchant_donau_instances IS 'Here we store information about individual Donau instances, including details about associated charities and donation limits';
    155   COMMENT ON COLUMN merchant_donau_instances.donau_instances_serial IS 'Unique serial identifier for each Donau instance';
    156   COMMENT ON COLUMN merchant_donau_instances.donau_url IS 'The URL associated with the Donau system for this instance';
    157   COMMENT ON COLUMN merchant_donau_instances.charity_name IS 'Name of the charity (for humans)';
    158   COMMENT ON COLUMN merchant_donau_instances.charity_id IS 'The unique identifier for the charity organization linked to this Donau instance';
    159   COMMENT ON COLUMN merchant_donau_instances.charity_max_per_year IS 'Maximum allowable donation amount per year for the charity associated with this instance, stored in taler_amount_currency';
    160   COMMENT ON COLUMN merchant_donau_instances.charity_receipts_to_date IS 'The total amount of donations received to date for this instance, stored in taler_amount_currency';
    161   COMMENT ON COLUMN merchant_donau_instances.current_year IS 'The current year for tracking donations for this instance, stored as an 8-byte integer';
    162 
    163   CREATE TABLE merchant_login_tokens (
    164     serial INT8 GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    165     token BYTEA NOT NULL UNIQUE,
    166     creation_time INT8 NOT NULL,
    167     expiration_time INT8 NOT NULL,
    168     validity_scope INT4 NOT NULL,
    169     description TEXT NOT NULL,
    170     CONSTRAINT merchant_login_tokens_token_check CHECK ((LENGTH(token) = 32))
    171   );
    172   COMMENT ON TABLE merchant_login_tokens IS 'login tokens that have been created for the given instance';
    173   COMMENT ON COLUMN merchant_login_tokens.token IS 'binary value of the login token';
    174   COMMENT ON COLUMN merchant_login_tokens.creation_time IS 'time when the token was created; currently not used, potentially useful in the future for a forced logout of all tokens issued before a certain date';
    175   COMMENT ON COLUMN merchant_login_tokens.expiration_time IS 'determines when the token expires';
    176   COMMENT ON COLUMN merchant_login_tokens.validity_scope IS 'identifies the operations for which the token is valid';
    177   COMMENT ON COLUMN merchant_login_tokens.description IS 'Description of the login token';
    178   CREATE INDEX merchant_login_tokens_by_expiration
    179     ON merchant_login_tokens (expiration_time);
    180 
    181   CREATE TABLE merchant_money_pots (
    182     money_pot_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    183     money_pot_name TEXT NOT NULL UNIQUE,
    184     money_pot_description TEXT NOT NULL,
    185     pot_totals merchant.taler_amount_currency[]
    186       DEFAULT ARRAY[]::merchant.taler_amount_currency[] NOT NULL
    187   );
    188   COMMENT ON TABLE merchant_money_pots IS 'Accounting construct for tracking finances by groups such as net income, taxes, tips to be paid to staff, etc.';
    189   COMMENT ON COLUMN merchant_money_pots.money_pot_serial IS 'Unique identifier for the money pot';
    190   COMMENT ON COLUMN merchant_money_pots.money_pot_name IS 'Name for the money pot';
    191   COMMENT ON COLUMN merchant_money_pots.money_pot_description IS 'Human-readable description for the money pot';
    192   COMMENT ON COLUMN merchant_money_pots.pot_totals IS 'Total amounts in the pot';
    193 
    194   CREATE TABLE merchant_orders (
    195     order_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    196     order_id TEXT NOT NULL UNIQUE,
    197     claim_token BYTEA NOT NULL,
    198     h_post_data BYTEA NOT NULL,
    199     pay_deadline INT8 NOT NULL,
    200     creation_time INT8 NOT NULL,
    201     contract_terms JSONB NOT NULL,
    202     pos_key TEXT,
    203     pos_algorithm INT4 DEFAULT 0 NOT NULL,
    204     fulfillment_url TEXT,
    205     session_id TEXT DEFAULT ''::text NOT NULL,
    206     CONSTRAINT merchant_orders_claim_token_check CHECK ((LENGTH(claim_token) = 16)),
    207     CONSTRAINT merchant_orders_h_post_data_check CHECK ((LENGTH(h_post_data) = 64))
    208   );
    209   COMMENT ON TABLE merchant_orders IS 'Orders we offered to a customer, but that have not yet been claimed';
    210   COMMENT ON COLUMN merchant_orders.order_id IS 'Unique identifier for the order';
    211   COMMENT ON COLUMN merchant_orders.claim_token IS 'Token optionally used to authorize the wallet to claim the order. All zeros (not NULL) if not used';
    212   COMMENT ON COLUMN merchant_orders.h_post_data IS 'Hash of the POST request that created this order, for idempotency checks';
    213   COMMENT ON COLUMN merchant_orders.pay_deadline IS 'How long is the offer valid. After this time, the order can be garbage collected';
    214   COMMENT ON COLUMN merchant_orders.creation_time IS 'time at which the order was originally created';
    215   COMMENT ON COLUMN merchant_orders.contract_terms IS 'Claiming changes the contract_terms, hence we have no hash of the terms in this table';
    216   COMMENT ON COLUMN merchant_orders.pos_key IS 'encoded based key which is used for the verification of payment';
    217   COMMENT ON COLUMN merchant_orders.pos_algorithm IS 'algorithm to used to generate the confirmation code. It is link with the pos_key';
    218   COMMENT ON COLUMN merchant_orders.fulfillment_url IS 'URL where the wallet will redirect the user upon payment';
    219   COMMENT ON COLUMN merchant_orders.session_id IS 'session_id to which the payment will be bound';
    220   CREATE INDEX merchant_orders_by_creation_time
    221     ON merchant_orders (creation_time);
    222   CREATE INDEX merchant_orders_by_expiration
    223     ON merchant_orders (pay_deadline);
    224   CREATE INDEX merchant_orders_by_merchant_and_fullfilment_and_session
    225     ON merchant_orders (fulfillment_url, session_id);
    226   CREATE INDEX merchant_orders_by_merchant_and_session
    227     ON merchant_orders (session_id);
    228   CREATE INDEX trgm_idx_order_summaries
    229     ON merchant_orders USING gin (lower((contract_terms ->> 'summary'::text)) public.gin_trgm_ops);
    230 
    231   CREATE TABLE merchant_otp_devices (
    232     otp_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    233     otp_id TEXT NOT NULL UNIQUE,
    234     otp_description TEXT NOT NULL,
    235     otp_key TEXT,
    236     otp_algorithm INT4 DEFAULT 0 NOT NULL,
    237     otp_ctr INT8 DEFAULT 0 NOT NULL
    238   );
    239   COMMENT ON TABLE merchant_otp_devices IS 'OTP device owned by a merchant';
    240   COMMENT ON COLUMN merchant_otp_devices.otp_id IS 'slug identifying the OTP device in protocols';
    241   COMMENT ON COLUMN merchant_otp_devices.otp_description IS 'Human-readable OTP device description';
    242   COMMENT ON COLUMN merchant_otp_devices.otp_key IS 'A base64-encoded key of the point-of-sale. It will be use by the OTP device';
    243   COMMENT ON COLUMN merchant_otp_devices.otp_algorithm IS 'algorithm to used to generate the confirmation code. It is linked with the otp_key and otp_ctr';
    244   COMMENT ON COLUMN merchant_otp_devices.otp_ctr IS 'counter for counter-based OTP generators';
    245 
    246   CREATE TABLE merchant_reports (
    247     report_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    248     report_program_section TEXT NOT NULL,
    249     report_description TEXT NOT NULL,
    250     mime_type TEXT NOT NULL,
    251     report_token BYTEA NOT NULL,
    252     data_source TEXT NOT NULL,
    253     target_address TEXT NOT NULL,
    254     frequency INT8 NOT NULL,
    255     frequency_shift INT8 NOT NULL,
    256     next_transmission INT8 NOT NULL,
    257     last_error_code INT4,
    258     last_error_detail TEXT,
    259     one_shot_hidden BOOLEAN DEFAULT FALSE,
    260     CONSTRAINT merchant_reports_report_token_check CHECK ((LENGTH(report_token) = 32))
    261   );
    262   COMMENT ON TABLE merchant_reports IS 'Specifies where we should send periodic reports about instance activities';
    263   COMMENT ON COLUMN merchant_reports.report_serial IS 'Unique identifier for the report';
    264   COMMENT ON COLUMN merchant_reports.report_program_section IS 'Which helper program (configuration section) to use to transmit the report';
    265   COMMENT ON COLUMN merchant_reports.report_description IS 'FIXME';
    266   COMMENT ON COLUMN merchant_reports.mime_type IS 'Mime-type to request from the backend for the transmission';
    267   COMMENT ON COLUMN merchant_reports.report_token IS 'Token clients requesting the report must include in the /report request';
    268   COMMENT ON COLUMN merchant_reports.data_source IS 'Relative URL of the instance for a GET request to request data to send';
    269   COMMENT ON COLUMN merchant_reports.target_address IS 'Address to which the report should be sent';
    270   COMMENT ON COLUMN merchant_reports.frequency IS 'Relative time with the desired report frequency';
    271   COMMENT ON COLUMN merchant_reports.frequency_shift IS 'Relative time by which to offset the actual transmission from the frequency multiple';
    272   COMMENT ON COLUMN merchant_reports.next_transmission IS 'Absolute time at which we should do the next transmission';
    273   COMMENT ON COLUMN merchant_reports.last_error_code IS 'ErrorCode of the last attempted transmission, NULL on success';
    274   COMMENT ON COLUMN merchant_reports.last_error_detail IS 'Additional human-readable text explaining errors from the last transmission attempt (for diagnostics), NULL on success';
    275   COMMENT ON COLUMN merchant_reports.one_shot_hidden IS 'True for reports that are only supposed to trigger once and that should be hidden from the user interface as they are internally generated. Used for alerts, such as those generated when the KYC status changes.';
    276 
    277   CREATE TABLE merchant_product_groups (
    278     product_group_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    279     product_group_name TEXT NOT NULL UNIQUE,
    280     product_group_description TEXT NOT NULL
    281   );
    282   COMMENT ON TABLE merchant_product_groups IS 'Specifies a product group';
    283   COMMENT ON COLUMN merchant_product_groups.product_group_serial IS 'Unique identifier for the group';
    284   COMMENT ON COLUMN merchant_product_groups.product_group_name IS 'Name for the group';
    285   COMMENT ON COLUMN merchant_product_groups.product_group_description IS 'Human-readable description for the group';
    286 
    287   CREATE TABLE merchant_statistic_bucket_meta (
    288     bmeta_serial_id INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    289     slug TEXT NOT NULL,
    290     description TEXT NOT NULL,
    291     stype merchant.statistic_type NOT NULL,
    292     ranges merchant.statistic_range[] NOT NULL,
    293     ages INT4[] NOT NULL,
    294     CONSTRAINT equal_array_LENGTH CHECK ((array_LENGTH(ranges, 1) = array_LENGTH(ages, 1))),
    295     UNIQUE (slug, stype)
    296   );
    297   COMMENT ON TABLE merchant_statistic_bucket_meta IS 'meta data about a statistic with events falling into buckets we are tracking';
    298   COMMENT ON COLUMN merchant_statistic_bucket_meta.bmeta_serial_id IS 'unique identifier for this type of bucket statistic we are tracking';
    299   COMMENT ON COLUMN merchant_statistic_bucket_meta.slug IS 'keyword (or name) of the statistic; identifies what the statistic is about; should be a slug suitable for a URI path';
    300   COMMENT ON COLUMN merchant_statistic_bucket_meta.description IS 'description of the statistic being tracked';
    301   COMMENT ON COLUMN merchant_statistic_bucket_meta.stype IS 'statistic type, what kind of data is being tracked, amount or number';
    302   COMMENT ON COLUMN merchant_statistic_bucket_meta.ranges IS 'size of the buckets that are being kept for this statistic';
    303   COMMENT ON COLUMN merchant_statistic_bucket_meta.ages IS 'determines how long into the past we keep buckets for the range at the given index around (in generations)';
    304 
    305   CREATE TABLE merchant_statistic_interval_meta (
    306     imeta_serial_id INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    307     slug TEXT NOT NULL,
    308     description TEXT NOT NULL,
    309     stype merchant.statistic_type NOT NULL,
    310     ranges INT8[] NOT NULL,
    311     precisions INT8[] NOT NULL,
    312     CONSTRAINT equal_array_LENGTH CHECK ((array_LENGTH(ranges, 1) = array_LENGTH(precisions, 1))),
    313     CONSTRAINT merchant_statistic_interval_meta_precisions_check CHECK ((array_LENGTH(precisions, 1) > 0)),
    314     CONSTRAINT merchant_statistic_interval_meta_ranges_check CHECK ((array_LENGTH(ranges, 1) > 0)),
    315     UNIQUE (slug, stype)
    316   );
    317   COMMENT ON TABLE merchant_statistic_interval_meta IS 'meta data about an interval statistic we are tracking';
    318   COMMENT ON COLUMN merchant_statistic_interval_meta.imeta_serial_id IS 'unique identifier for this type of interval statistic we are tracking';
    319   COMMENT ON COLUMN merchant_statistic_interval_meta.slug IS 'keyword (or name) of the statistic; identifies what the statistic is about; should be a slug suitable for a URI path';
    320   COMMENT ON COLUMN merchant_statistic_interval_meta.description IS 'description of the statistic being tracked';
    321   COMMENT ON COLUMN merchant_statistic_interval_meta.stype IS 'statistic type, what kind of data is being tracked, amount or number';
    322   COMMENT ON COLUMN merchant_statistic_interval_meta.ranges IS 'range of values that is being kept for this statistic, in seconds, must be monotonically increasing';
    323   COMMENT ON COLUMN merchant_statistic_interval_meta.precisions IS 'determines how precisely we track which events fall into the range at the same index (allowing us to coalesce events with timestamps in proximity close to the given precision), in seconds, 0 is not allowed';
    324 
    325   CREATE TABLE merchant_token_families (
    326     token_family_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    327     slug TEXT NOT NULL UNIQUE,
    328     name TEXT NOT NULL,
    329     description TEXT,
    330     description_i18n JSONB NOT NULL,
    331     valid_after INT8 NOT NULL,
    332     valid_before INT8 NOT NULL,
    333     duration INT8 NOT NULL,
    334     kind TEXT NOT NULL,
    335     issued INT8 DEFAULT 0,
    336     used INT8 DEFAULT 0,
    337     validity_granularity INT8 DEFAULT '2592000000000'::INT8 NOT NULL,
    338     start_offset INT8 DEFAULT 0 NOT NULL,
    339     cipher_choice TEXT DEFAULT 'rsa(2048)'::text NOT NULL,
    340     extra_data JSONB,
    341     CONSTRAINT merchant_token_families_kind_check
    342       CHECK ((kind = ANY (ARRAY['subscription'::text, 'discount'::text]))),
    343     CONSTRAINT merchant_token_families_validity_granularity_check
    344       CHECK ((validity_granularity = ANY (ARRAY[(60000000)::INT8,
    345         '3600000000'::INT8, '86400000000'::INT8, '604800000000'::INT8,
    346         '2592000000000'::INT8, '7776000000000'::INT8,
    347         '31536000000000'::INT8])))
    348   );
    349   COMMENT ON TABLE merchant_token_families IS 'Token families configured by the merchant.';
    350   COMMENT ON COLUMN merchant_token_families.slug IS 'Unique slug for the token family.';
    351   COMMENT ON COLUMN merchant_token_families.name IS 'Name of the token family.';
    352   COMMENT ON COLUMN merchant_token_families.description IS 'Human-readable description or details about the token family.';
    353   COMMENT ON COLUMN merchant_token_families.description_i18n IS 'JSON map from IETF BCP 47 language tags to localized descriptions';
    354   COMMENT ON COLUMN merchant_token_families.valid_after IS 'Start time of the token family''s validity period.';
    355   COMMENT ON COLUMN merchant_token_families.valid_before IS 'End time of the token family''s validity period.';
    356   COMMENT ON COLUMN merchant_token_families.duration IS 'Duration of the token.';
    357   COMMENT ON COLUMN merchant_token_families.kind IS 'Kind of the token (e.g., subscription, discount).';
    358   COMMENT ON COLUMN merchant_token_families.issued IS 'Counter for the number of tokens issued for this token family.';
    359   COMMENT ON COLUMN merchant_token_families.used IS 'FIXME';
    360   COMMENT ON COLUMN merchant_token_families.validity_granularity IS 'To compute key lifetimes, we first round the payment deadline down to a multiple of this time; supported values are one minute, one hour, a day, a week, 30 days, 90 days or a year (indicatited using 365 days); adding the start_offset gets the start validity time; adding the duration to get the signature_valid_until value for the key';
    361   COMMENT ON COLUMN merchant_token_families.start_offset IS 'This allows shifting the validity period of signatures to start a bit before the time rounded to the precision. For example, Swiss vignettes are valid for 14 months, from December of year X to January of year X+2. This can be achieve by setting a start_offset of 30 days, and a duration of 14 months and a precision of 1 year. The value given is in microseconds (but will be rounded to seconds).';
    362   COMMENT ON COLUMN merchant_token_families.cipher_choice IS 'Specifies the type of cipher that should be used for this token family. Currently supported values are "cs" and "rsa($LEN)" where $LEN is the key length in bits.';
    363   COMMENT ON COLUMN merchant_token_families.extra_data IS 'JSON field with family-specific meta data, such as the trusted_domains for subscriptions or expected_domains for discount tokens';
    364 
    365   CREATE TABLE merchant_unclaim_signatures (
    366     unclaim_serial INT8 GENERATED BY DEFAULT AS IDENTITY UNIQUE,
    367     h_contract_terms BYTEA NOT NULL,
    368     unclaim_sig BYTEA NOT NULL PRIMARY KEY,
    369     expiration_time INT8 NOT NULL,
    370     CONSTRAINT merchant_unclaim_signatures_h_contract_terms_check CHECK ((LENGTH(h_contract_terms) = 64)),
    371     CONSTRAINT merchant_unclaim_signatures_unclaim_sig_check CHECK ((LENGTH(unclaim_sig) = 64))
    372   );
    373   COMMENT ON TABLE merchant_unclaim_signatures IS 'Here we store proofs of wallets unclaiming a claim to a contract. Right now not really used, but theoretically legally important as this shows that the wallet was the one abandoing its rights to the proposal.';
    374   COMMENT ON COLUMN merchant_unclaim_signatures.h_contract_terms IS 'Hash over the contract terms of the unclaimed contract';
    375   COMMENT ON COLUMN merchant_unclaim_signatures.unclaim_sig IS 'Signature of purpose CONTRACT_UNCLAIM';
    376   COMMENT ON COLUMN merchant_unclaim_signatures.expiration_time IS 'Payment deadline of the original contract, at this point we should be OK to delete the unclaim signature via garbage collection.';
    377   CREATE INDEX merchant_unclaim_signatures_by_expiration
    378     ON merchant_unclaim_signatures (expiration_time);
    379   COMMENT ON INDEX merchant_unclaim_signatures_by_expiration IS 'For garbage collection of unclaim signatrues after payment deadlines have been passed.';
    380 
    381   CREATE TABLE merchant_webhook (
    382     webhook_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    383     webhook_id TEXT NOT NULL UNIQUE,
    384     event_type TEXT NOT NULL,
    385     url TEXT NOT NULL,
    386     http_method TEXT NOT NULL,
    387     header_template TEXT,
    388     body_template TEXT
    389   );
    390   COMMENT ON TABLE merchant_webhook IS 'webhook used by the merchant (may be incomplete, frontend can override)';
    391   COMMENT ON COLUMN merchant_webhook.webhook_id IS 'slug identifying the webhook in protocols';
    392   COMMENT ON COLUMN merchant_webhook.event_type IS 'Event of the webhook';
    393   COMMENT ON COLUMN merchant_webhook.url IS 'URL to make the request to';
    394   COMMENT ON COLUMN merchant_webhook.http_method IS 'http method use by the merchant';
    395   COMMENT ON COLUMN merchant_webhook.header_template IS 'Template for the header of the webhook, to be modified based on trigger data';
    396   COMMENT ON COLUMN merchant_webhook.body_template IS 'Template for the body of the webhook, to be modified based on trigger data';
    397 
    398   CREATE TABLE tan_challenges (
    399     challenge_id INT8 GENERATED BY DEFAULT AS IDENTITY UNIQUE,
    400     h_body BYTEA NOT NULL,
    401     salt BYTEA NOT NULL,
    402     op merchant.op_enum NOT NULL,
    403     code TEXT NOT NULL,
    404     creation_date INT8 NOT NULL,
    405     expiration_date INT8 NOT NULL,
    406     retransmission_date INT8 DEFAULT 0 NOT NULL,
    407     confirmation_date INT8,
    408     retry_counter INT4 NOT NULL,
    409     tan_channel merchant.tan_enum NOT NULL,
    410     required_address TEXT NOT NULL,
    411     CONSTRAINT tan_challenges_h_body_check CHECK ((LENGTH(h_body) = 32)),
    412     CONSTRAINT tan_challenges_salt_check CHECK ((LENGTH(salt) = 16))
    413   );
    414   COMMENT ON TABLE tan_challenges IS 'Stores multi-factor authorization (MFA) challenges';
    415   COMMENT ON COLUMN tan_challenges.challenge_id IS 'Unique identifier for the challenge';
    416   COMMENT ON COLUMN tan_challenges.h_body IS 'Salted hash of the body of the original request that triggered the challenge, to be replayed once the challenge is satisfied.';
    417   COMMENT ON COLUMN tan_challenges.salt IS 'Salt used when hashing the original body.';
    418   COMMENT ON COLUMN tan_challenges.op IS 'The protected operation to run after the challenge';
    419   COMMENT ON COLUMN tan_challenges.code IS 'The pin code sent to the user and verified';
    420   COMMENT ON COLUMN tan_challenges.creation_date IS 'Creation date of the code';
    421   COMMENT ON COLUMN tan_challenges.expiration_date IS 'When will the code expire';
    422   COMMENT ON COLUMN tan_challenges.retransmission_date IS 'When did we last transmit the challenge to the user';
    423   COMMENT ON COLUMN tan_challenges.confirmation_date IS 'When was this challenge successfully verified, NULL if pending';
    424   COMMENT ON COLUMN tan_challenges.retry_counter IS 'How many tries are left for this code; must be > 0';
    425   COMMENT ON COLUMN tan_challenges.tan_channel IS 'TAN channel to use, if NULL use customer configured one';
    426   COMMENT ON COLUMN tan_challenges.required_address IS 'Address to which the challenge will be sent';
    427   CREATE INDEX tan_challenges_expiration_index
    428     ON tan_challenges (expiration_date);
    429   COMMENT ON INDEX tan_challenges_expiration_index IS 'for garbage collection';
    430 
    431   CREATE TABLE merchant_builtin_unit_overrides (
    432     builtin_unit_serial INT8 NOT NULL PRIMARY KEY,
    433     override_allow_fraction BOOLEAN,
    434     override_precision_level INT4,
    435     override_active BOOLEAN,
    436     CONSTRAINT merchant_builtin_unit_overrides_override_precision_level_check
    437       CHECK (((override_precision_level >= 0) AND (override_precision_level <= 6))),
    438     CONSTRAINT merchant_builtin_unit_overrides_builtin_unit_serial_fkey
    439       FOREIGN KEY (builtin_unit_serial)
    440       REFERENCES merchant.merchant_builtin_units(unit_serial) ON DELETE CASCADE
    441   );
    442   COMMENT ON TABLE merchant_builtin_unit_overrides IS 'Per-instance overrides for builtin units (fraction policy and visibility).';
    443   COMMENT ON COLUMN merchant_builtin_unit_overrides.override_allow_fraction IS 'FIXME';
    444   COMMENT ON COLUMN merchant_builtin_unit_overrides.override_precision_level IS 'FIXME';
    445   COMMENT ON COLUMN merchant_builtin_unit_overrides.override_active IS 'FIXME';
    446 
    447   -- ===================================================================
    448   -- Tier 1: tables referencing only Tier 0 (and global merchant.*) tables.
    449   -- ===================================================================
    450 
    451   CREATE TABLE merchant_deposit_confirmations (
    452     deposit_confirmation_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    453     order_serial INT8,
    454     deposit_timestamp INT8 NOT NULL,
    455     exchange_url TEXT NOT NULL,
    456     total_without_fee merchant.taler_amount_currency NOT NULL,
    457     wire_fee merchant.taler_amount_currency NOT NULL,
    458     signkey_serial INT8 NOT NULL,
    459     exchange_sig BYTEA NOT NULL,
    460     account_serial INT8 NOT NULL,
    461     wire_transfer_deadline INT8 DEFAULT 0 NOT NULL,
    462     wire_pending BOOLEAN DEFAULT TRUE NOT NULL,
    463     exchange_failure TEXT,
    464     retry_backoff INT8 DEFAULT 0 NOT NULL,
    465     CONSTRAINT merchant_deposit_confirmations_exchange_sig_check CHECK ((LENGTH(exchange_sig) = 64)),
    466     UNIQUE (order_serial, exchange_sig),
    467     CONSTRAINT merchant_deposit_confirmations_account_serial_fkey
    468       FOREIGN KEY (account_serial)
    469       REFERENCES merchant_accounts(account_serial) ON DELETE CASCADE,
    470     CONSTRAINT merchant_deposit_confirmations_order_serial_fkey
    471       FOREIGN KEY (order_serial)
    472       REFERENCES merchant_contract_terms(order_serial) ON DELETE CASCADE,
    473     CONSTRAINT merchant_deposit_confirmations_signkey_serial_fkey
    474       FOREIGN KEY (signkey_serial)
    475       REFERENCES merchant.merchant_exchange_signing_keys(signkey_serial) ON DELETE CASCADE
    476   );
    477   COMMENT ON TABLE merchant_deposit_confirmations IS 'Table with the deposit confirmations for each coin we deposited at the exchange';
    478   COMMENT ON COLUMN merchant_deposit_confirmations.order_serial IS 'Identifies the entry in the contract_terms table that was paid by this deposit';
    479   COMMENT ON COLUMN merchant_deposit_confirmations.deposit_timestamp IS 'Time when the exchange generated the deposit confirmation';
    480   COMMENT ON COLUMN merchant_deposit_confirmations.exchange_url IS 'Which exchange received the deposit; a single contract may be paid by multiple (batch) deposits to multiple exchanges';
    481   COMMENT ON COLUMN merchant_deposit_confirmations.total_without_fee IS 'Total amount that was (batch) deposited, excluding deposit fees (irrespective of who had to pay the fees); so this is the amount the merchant will be credited for this deposit (minus applicable wire fees after aggregation)';
    482   COMMENT ON COLUMN merchant_deposit_confirmations.wire_fee IS 'We MAY want to see if we should try to get this via merchant_exchange_wire_fees (not sure, may be too complicated with the date range, etc.)';
    483   COMMENT ON COLUMN merchant_deposit_confirmations.signkey_serial IS 'Online signing key of the exchange on the deposit confirmation';
    484   COMMENT ON COLUMN merchant_deposit_confirmations.exchange_sig IS 'Signature of the exchange over the deposit confirmation';
    485   COMMENT ON COLUMN merchant_deposit_confirmations.account_serial IS 'Identifies the bank account of the merchant that will receive the payment';
    486   COMMENT ON COLUMN merchant_deposit_confirmations.wire_transfer_deadline IS 'when should the exchange make the wire transfer at the latest';
    487   COMMENT ON COLUMN merchant_deposit_confirmations.wire_pending IS 'true if we are awaiting wire details for a deposit of this purchase (and are not blocked on KYC); false once the exchange says that the wire transfer has happened (does not mean that we confirmed it happened though)';
    488   COMMENT ON COLUMN merchant_deposit_confirmations.exchange_failure IS 'Text describing exchange failures in making timely wire transfers for this deposit confirmation';
    489   COMMENT ON COLUMN merchant_deposit_confirmations.retry_backoff IS 'exponentially increasing value we add to the wire_transfer_deadline on each failure to confirm the wire transfer';
    490   CREATE INDEX merchant_deposit_confirmations_by_pending_wire
    491     ON merchant_deposit_confirmations (exchange_url, wire_transfer_deadline)
    492     WHERE wire_pending;
    493 
    494   CREATE TABLE merchant_expected_transfers (
    495     expected_credit_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    496     exchange_url TEXT NOT NULL,
    497     wtid BYTEA NOT NULL,
    498     expected_credit_amount merchant.taler_amount_currency,
    499     wire_fee merchant.taler_amount_currency,
    500     account_serial INT8 NOT NULL,
    501     expected_time INT8 NOT NULL,
    502     retry_time INT8 DEFAULT 0 NOT NULL,
    503     last_http_status INT4,
    504     last_ec INT4,
    505     last_detail TEXT,
    506     retry_needed BOOLEAN DEFAULT TRUE NOT NULL,
    507     signkey_serial INT8,
    508     exchange_sig BYTEA,
    509     h_details BYTEA,
    510     confirmed BOOLEAN DEFAULT FALSE NOT NULL,
    511     CONSTRAINT merchant_expected_transfers_exchange_sig_check CHECK ((LENGTH(exchange_sig) = 64)),
    512     CONSTRAINT merchant_expected_transfers_h_details_check CHECK ((LENGTH(h_details) = 64)),
    513     CONSTRAINT merchant_expected_transfers_wtid_check CHECK ((LENGTH(wtid) = 32)),
    514     UNIQUE (wtid, exchange_url, account_serial),
    515     CONSTRAINT merchant_expected_transfers_account_serial_fkey
    516       FOREIGN KEY (account_serial)
    517       REFERENCES merchant_accounts(account_serial) ON DELETE CASCADE,
    518     CONSTRAINT merchant_expected_transfers_signkey_serial_fkey
    519       FOREIGN KEY (signkey_serial)
    520       REFERENCES merchant.merchant_exchange_signing_keys(signkey_serial) ON DELETE CASCADE
    521   );
    522   COMMENT ON TABLE merchant_expected_transfers IS 'expected incoming wire transfers';
    523   COMMENT ON COLUMN merchant_expected_transfers.expected_credit_serial IS 'Unique identifier for this expected wire transfer in this backend';
    524   COMMENT ON COLUMN merchant_expected_transfers.exchange_url IS 'Base URL of the exchange that originated the wire transfer as extracted from the wire transfer subject';
    525   COMMENT ON COLUMN merchant_expected_transfers.wtid IS 'Unique wire transfer identifier (or at least, should be unique by protocol) as selected by the exchange and extracted from the wire transfer subject';
    526   COMMENT ON COLUMN merchant_expected_transfers.expected_credit_amount IS 'expected actual value of the (aggregated) wire transfer, excluding the wire fee; NULL if unknown';
    527   COMMENT ON COLUMN merchant_expected_transfers.wire_fee IS 'wire fee the exchange claims to have charged us; NULL if unknown';
    528   COMMENT ON COLUMN merchant_expected_transfers.account_serial IS 'Merchant bank account that should receive this wire transfer; also implies the merchant instance implicated by the wire transfer';
    529   COMMENT ON COLUMN merchant_expected_transfers.expected_time IS 'Time when we should expect the exchange do do the wire transfer';
    530   COMMENT ON COLUMN merchant_expected_transfers.retry_time IS 'Time when we should next inquire at the exchange about this wire transfer; used by taler-merchant-reconciliation to limit retries with the exchange in case of failures';
    531   COMMENT ON COLUMN merchant_expected_transfers.last_http_status IS 'HTTP status of the last request to the exchange, 0 on timeout or if there was no request (200 on success)';
    532   COMMENT ON COLUMN merchant_expected_transfers.last_ec IS 'Taler error code from the last request to the exchange, 0 on success or if there was no request';
    533   COMMENT ON COLUMN merchant_expected_transfers.last_detail IS 'Taler error detail from the last request to the exchange, NULL on success or if there was no request';
    534   COMMENT ON COLUMN merchant_expected_transfers.retry_needed IS 'true if we need to retry the HTTP request to the exchange (never did it, or transient failure)';
    535   COMMENT ON COLUMN merchant_expected_transfers.signkey_serial IS 'Identifies the online signing key of the exchange used to make the exchange_sig';
    536   COMMENT ON COLUMN merchant_expected_transfers.exchange_sig IS 'Signature over the aggregation response from the exchange, or NULL on error or if we did not yet make that request';
    537   COMMENT ON COLUMN merchant_expected_transfers.h_details IS 'Hash over the aggregation details returned by the exchange, provided here for fast exchange_sig validation';
    538   COMMENT ON COLUMN merchant_expected_transfers.confirmed IS 'true once the merchant confirmed that this transfer was received and a matching transfer exists in the merchant_transfers table; set automatically via INSERT TRIGGER merchant_expected_transfers_insert_trigger';
    539   CREATE INDEX merchant_expected_transfers_by_open
    540     ON merchant_expected_transfers (retry_time)
    541     WHERE ((NOT confirmed) OR retry_needed);
    542   COMMENT ON INDEX merchant_expected_transfers_by_open IS 'For select_open_transfers';
    543 
    544   CREATE TABLE merchant_inventory (
    545     product_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    546     product_id TEXT NOT NULL UNIQUE,
    547     description TEXT NOT NULL,
    548     description_i18n JSONB NOT NULL,
    549     unit TEXT NOT NULL,
    550     image TEXT NOT NULL,
    551     taxes JSONB NOT NULL,
    552     total_stock INT8 NOT NULL,
    553     total_sold INT8 DEFAULT 0 NOT NULL,
    554     total_lost INT8 DEFAULT 0 NOT NULL,
    555     address JSONB NOT NULL,
    556     next_restock INT8 NOT NULL,
    557     minimum_age INT4 DEFAULT 0 NOT NULL,
    558     product_name TEXT NOT NULL,
    559     image_hash TEXT,
    560     price_array merchant.taler_amount_currency[]
    561       DEFAULT ARRAY[]::merchant.taler_amount_currency[] NOT NULL,
    562     total_stock_frac INT4 DEFAULT 0 NOT NULL,
    563     total_sold_frac INT4 DEFAULT 0 NOT NULL,
    564     total_lost_frac INT4 DEFAULT 0 NOT NULL,
    565     allow_fractional_quantity BOOLEAN DEFAULT FALSE NOT NULL,
    566     fractional_precision_level INT4 DEFAULT 0 NOT NULL,
    567     product_group_serial INT8,
    568     money_pot_serial INT8,
    569     price_is_net BOOLEAN DEFAULT FALSE,
    570     CONSTRAINT merchant_inventory_money_pot_serial_fkey
    571       FOREIGN KEY (money_pot_serial)
    572       REFERENCES merchant_money_pots(money_pot_serial) ON DELETE SET NULL,
    573     CONSTRAINT merchant_inventory_product_group_serial_fkey
    574       FOREIGN KEY (product_group_serial)
    575       REFERENCES merchant_product_groups(product_group_serial) ON DELETE SET NULL
    576   );
    577   COMMENT ON TABLE merchant_inventory IS 'products offered by the merchant (may be incomplete, frontend can override)';
    578   COMMENT ON COLUMN merchant_inventory.product_id IS 'slug identifying the product in protocols';
    579   COMMENT ON COLUMN merchant_inventory.description IS 'Human-readable product description';
    580   COMMENT ON COLUMN merchant_inventory.description_i18n IS 'JSON map from IETF BCP 47 language tags to localized descriptions';
    581   COMMENT ON COLUMN merchant_inventory.unit IS 'Unit of sale for the product (liters, kilograms, packages)';
    582   COMMENT ON COLUMN merchant_inventory.image IS 'NOT NULL, but can be 0 bytes; must contain an ImageDataUrl';
    583   COMMENT ON COLUMN merchant_inventory.taxes IS 'JSON array containing taxes the merchant pays, must be JSON, but can be just "[]"';
    584   COMMENT ON COLUMN merchant_inventory.total_stock IS 'A value of -1 is used for unlimited (electronic good), may never be lowered';
    585   COMMENT ON COLUMN merchant_inventory.total_sold IS 'Number of products sold, must be below total_stock, non-negative, may never be lowered';
    586   COMMENT ON COLUMN merchant_inventory.total_lost IS 'Number of products that used to be in stock but were lost (spoiled, damaged), may never be lowered; total_stock >= total_sold + total_lost must always hold';
    587   COMMENT ON COLUMN merchant_inventory.address IS 'JSON formatted Location of where the product is stocked';
    588   COMMENT ON COLUMN merchant_inventory.next_restock IS 'GNUnet absolute time i ndicating when the next restock is expected. 0 for unknown.';
    589   COMMENT ON COLUMN merchant_inventory.minimum_age IS 'Minimum age of the customer in years, to be used if an exchange supports the age restriction extension.';
    590   COMMENT ON COLUMN merchant_inventory.product_name IS 'Name of the product';
    591   COMMENT ON COLUMN merchant_inventory.image_hash IS 'SHA-256 hash of the base64-encoded image data, used by wallets to fetch product images.';
    592   COMMENT ON COLUMN merchant_inventory.price_array IS 'List of unit prices available for the product (multiple tiers supported).';
    593   COMMENT ON COLUMN merchant_inventory.total_stock_frac IS 'Fractional part of stock in units of 1/1000000 of the base value';
    594   COMMENT ON COLUMN merchant_inventory.total_sold_frac IS 'Fractional part of units sold in units of 1/1000000 of the base value';
    595   COMMENT ON COLUMN merchant_inventory.total_lost_frac IS 'Fractional part of units lost in units of 1/1000000 of the base value';
    596   COMMENT ON COLUMN merchant_inventory.allow_fractional_quantity IS 'Whether fractional stock (total_stock_frac) should be honored for this product';
    597   COMMENT ON COLUMN merchant_inventory.fractional_precision_level IS 'Preset number of decimal places for fractional quantities';
    598   COMMENT ON COLUMN merchant_inventory.product_group_serial IS 'Specifies the product group a given product is a member of. If NULL, the product is in the __default__ group';
    599   COMMENT ON COLUMN merchant_inventory.money_pot_serial IS 'Specifies that sales amounts of this product (excluding taxes on the product that have their own pot specified) should by default be added to the given money pot. If NULL, the money pot rules of the overall order apply instead.';
    600   COMMENT ON COLUMN merchant_inventory.price_is_net IS 'If true, the price given is the net price; if false, it is the gross price.';
    601   CREATE INDEX merchant_inventory_by_image_hash
    602     ON merchant_inventory (image_hash);
    603   CREATE INDEX trgm_idx_products_by_description
    604     ON merchant_inventory USING gin (lower(description) public.gin_trgm_ops);
    605   CREATE INDEX trgm_idx_products_by_name
    606     ON merchant_inventory USING gin (lower(product_name) public.gin_trgm_ops);
    607 
    608   CREATE TABLE merchant_kyc (
    609     kyc_serial_id INT8 GENERATED BY DEFAULT AS IDENTITY UNIQUE,
    610     kyc_timestamp INT8 NOT NULL,
    611     kyc_ok BOOLEAN DEFAULT FALSE NOT NULL,
    612     account_serial INT8 NOT NULL,
    613     exchange_url TEXT NOT NULL,
    614     access_token BYTEA,
    615     exchange_http_status INT4 DEFAULT 0,
    616     exchange_ec_code INT4 DEFAULT 0,
    617     aml_review BOOLEAN DEFAULT FALSE,
    618     jaccount_limits JSONB,
    619     last_rule_gen INT8 DEFAULT 0 NOT NULL,
    620     next_kyc_poll INT8 DEFAULT 0 NOT NULL,
    621     kyc_backoff INT8 DEFAULT 0 NOT NULL,
    622     CONSTRAINT access_token_LENGTH_check CHECK ((LENGTH(access_token) = 32)),
    623     PRIMARY KEY (account_serial, exchange_url),
    624     CONSTRAINT merchant_kyc_account_serial_fkey
    625       FOREIGN KEY (account_serial)
    626       REFERENCES merchant_accounts(account_serial) ON DELETE CASCADE
    627   );
    628   COMMENT ON TABLE merchant_kyc IS 'Status of the KYC process of a merchant account at an exchange';
    629   COMMENT ON COLUMN merchant_kyc.kyc_timestamp IS 'Last time we checked our KYC status at the exchange. Useful to re-check if the status is very stale. Also the timestamp used for the exchange signature (if present).';
    630   COMMENT ON COLUMN merchant_kyc.kyc_ok IS 'true if the KYC check was passed successfully';
    631   COMMENT ON COLUMN merchant_kyc.account_serial IS 'Which bank account of the merchant is the KYC status for';
    632   COMMENT ON COLUMN merchant_kyc.exchange_url IS 'Which exchange base URL is this KYC status valid for';
    633   COMMENT ON COLUMN merchant_kyc.access_token IS 'Access token required to begin the KYC process';
    634   COMMENT ON COLUMN merchant_kyc.exchange_http_status IS 'Last HTTP status returned by the exchange when inquiring about our KYC status.';
    635   COMMENT ON COLUMN merchant_kyc.exchange_ec_code IS 'Last Taler error code returned by the exchange when inquiring about our KYC status.';
    636   COMMENT ON COLUMN merchant_kyc.aml_review IS 'True if our account is under AML review according to the exchange.';
    637   COMMENT ON COLUMN merchant_kyc.jaccount_limits IS 'JSON with AccountLimits that apply to this account';
    638   COMMENT ON COLUMN merchant_kyc.last_rule_gen IS 'Row ID from the exchange identifying the last decision change to KYC rules on this account. To be used when long-polling to ask for changes. 0 if no custom decisions were taken for the account';
    639   COMMENT ON COLUMN merchant_kyc.next_kyc_poll IS 'When should we next do a KYC poll on this exchange and bank account';
    640   COMMENT ON COLUMN merchant_kyc.kyc_backoff IS 'What is the current backoff value between KYC polls';
    641   CREATE INDEX merchant_kyc_by_next_kyc_poll
    642     ON merchant_kyc (next_kyc_poll);
    643 
    644   CREATE TABLE merchant_order_token_blinded_sigs (
    645     order_token_bs_serial INT8 GENERATED BY DEFAULT AS IDENTITY,
    646     order_serial INT8 NOT NULL,
    647     token_index INT4 NOT NULL,
    648     token_blinded_signature BYTEA NOT NULL,
    649     token_hash BYTEA NOT NULL,
    650     CONSTRAINT merchant_order_token_blinded_sigs_token_hash_check CHECK ((LENGTH(token_hash) = 64)),
    651     PRIMARY KEY (order_serial, token_index),
    652     CONSTRAINT merchant_order_token_blinded_sigs_order_serial_fkey
    653       FOREIGN KEY (order_serial)
    654       REFERENCES merchant_contract_terms(order_serial) ON DELETE CASCADE
    655   );
    656   COMMENT ON TABLE merchant_order_token_blinded_sigs IS 'Table linking merchant orders with Donau BUDIS information';
    657   COMMENT ON COLUMN merchant_order_token_blinded_sigs.order_token_bs_serial IS 'Unique serial identifier for token order linkage';
    658   COMMENT ON COLUMN merchant_order_token_blinded_sigs.order_serial IS 'Foreign key linking to the corresponding merchant order';
    659   COMMENT ON COLUMN merchant_order_token_blinded_sigs.token_index IS 'offset of the given signature in the output token array';
    660   COMMENT ON COLUMN merchant_order_token_blinded_sigs.token_blinded_signature IS 'Blinded signature of the token associated with the order';
    661   COMMENT ON COLUMN merchant_order_token_blinded_sigs.token_hash IS 'Hash of the token';
    662 
    663   CREATE TABLE merchant_pending_webhooks (
    664     webhook_pending_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    665     webhook_serial INT8 NOT NULL,
    666     next_attempt INT8 DEFAULT 0 NOT NULL,
    667     retries INT4 DEFAULT 0 NOT NULL,
    668     url TEXT NOT NULL,
    669     http_method TEXT NOT NULL,
    670     header TEXT,
    671     body TEXT,
    672     CONSTRAINT merchant_pending_webhooks_webhook_serial_fkey
    673       FOREIGN KEY (webhook_serial)
    674       REFERENCES merchant_webhook(webhook_serial) ON DELETE CASCADE
    675   );
    676   COMMENT ON TABLE merchant_pending_webhooks IS 'webhooks that still need to be executed by the merchant';
    677   COMMENT ON COLUMN merchant_pending_webhooks.webhook_serial IS 'Reference to the configured webhook template';
    678   COMMENT ON COLUMN merchant_pending_webhooks.next_attempt IS 'Time when we should make the next request to the webhook';
    679   COMMENT ON COLUMN merchant_pending_webhooks.retries IS 'How often have we tried this request so far';
    680   COMMENT ON COLUMN merchant_pending_webhooks.url IS 'URL to make the request to';
    681   COMMENT ON COLUMN merchant_pending_webhooks.http_method IS 'http method use for the webhook';
    682   COMMENT ON COLUMN merchant_pending_webhooks.header IS 'Header of the webhook';
    683   COMMENT ON COLUMN merchant_pending_webhooks.body IS 'Body of the webhook';
    684 
    685   CREATE TABLE merchant_refunds (
    686     refund_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    687     order_serial INT8 NOT NULL,
    688     rtransaction_id INT8 NOT NULL,
    689     refund_timestamp INT8 NOT NULL,
    690     coin_pub BYTEA NOT NULL,
    691     reason TEXT NOT NULL,
    692     refund_amount merchant.taler_amount_currency NOT NULL,
    693     UNIQUE (order_serial, coin_pub, rtransaction_id),
    694     CONSTRAINT merchant_refunds_order_serial_fkey
    695       FOREIGN KEY (order_serial)
    696       REFERENCES merchant_contract_terms(order_serial) ON DELETE CASCADE
    697   );
    698   COMMENT ON TABLE merchant_refunds IS 'Refunds approved by the merchant (backoffice) logic, excludes abort refunds';
    699   COMMENT ON COLUMN merchant_refunds.order_serial IS 'order for which the refund is being applied';
    700   COMMENT ON COLUMN merchant_refunds.rtransaction_id IS 'Needed for uniqueness in case a refund is increased for the same order';
    701   COMMENT ON COLUMN merchant_refunds.refund_timestamp IS 'Needed for grouping of refunds in the wallet UI; has no semantics in the protocol (only for UX), but should be from the time when the merchant internally approved the refund';
    702   COMMENT ON COLUMN merchant_refunds.coin_pub IS 'public key of the coin that is going to be refunded';
    703   COMMENT ON COLUMN merchant_refunds.reason IS 'human-readable reason given for the refund';
    704   COMMENT ON COLUMN merchant_refunds.refund_amount IS 'refund amount granted on this coin';
    705   CREATE INDEX merchant_refunds_by_coin_and_order
    706     ON merchant_refunds (coin_pub, order_serial);
    707 
    708   CREATE TABLE merchant_statistic_amount_event (
    709     aevent_serial_id INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    710     imeta_serial_id INT8,
    711     slot INT8 NOT NULL,
    712     delta_curr character varying(12) NOT NULL,
    713     delta_value INT8 NOT NULL,
    714     delta_frac INT4 NOT NULL,
    715     CONSTRAINT event_key UNIQUE (imeta_serial_id, delta_curr, slot),
    716     CONSTRAINT merchant_statistic_amount_event_imeta_serial_id_fkey
    717       FOREIGN KEY (imeta_serial_id)
    718       REFERENCES merchant_statistic_interval_meta(imeta_serial_id) ON DELETE CASCADE
    719   );
    720   COMMENT ON TABLE merchant_statistic_amount_event IS 'amount to decrement an interval statistic by when a certain time value is reached';
    721   COMMENT ON COLUMN merchant_statistic_amount_event.aevent_serial_id IS 'unique identifier for this amount event';
    722   COMMENT ON COLUMN merchant_statistic_amount_event.imeta_serial_id IS 'identifies what the statistic is about; must be of clazz interval and of stype amount';
    723   COMMENT ON COLUMN merchant_statistic_amount_event.slot IS 'identifies the time slot at which the given event(s) happened';
    724   COMMENT ON COLUMN merchant_statistic_amount_event.delta_curr IS 'currency of the total cumulative amount that was added at the time identified by slot';
    725   COMMENT ON COLUMN merchant_statistic_amount_event.delta_value IS 'total cumulative amount (value) that was added at the time identified by slot';
    726   COMMENT ON COLUMN merchant_statistic_amount_event.delta_frac IS 'total cumulative amount (fraction) that was added at the time identified by slot';
    727 
    728   CREATE TABLE merchant_statistic_bucket_amount (
    729     bmeta_serial_id INT8 NOT NULL,
    730     bucket_start INT8 NOT NULL,
    731     bucket_range merchant.statistic_range NOT NULL,
    732     curr character varying(12) NOT NULL,
    733     cumulative_value INT8 NOT NULL,
    734     cumulative_frac INT4 NOT NULL,
    735     PRIMARY KEY (bmeta_serial_id, curr, bucket_start, bucket_range),
    736     CONSTRAINT merchant_statistic_bucket_amount_bmeta_serial_id_fkey
    737       FOREIGN KEY (bmeta_serial_id)
    738       REFERENCES merchant_statistic_bucket_meta(bmeta_serial_id) ON DELETE CASCADE
    739   );
    740   COMMENT ON TABLE merchant_statistic_bucket_amount IS 'various amount statistics (in various currencies) being tracked';
    741   COMMENT ON COLUMN merchant_statistic_bucket_amount.bmeta_serial_id IS 'identifies what the statistic is about';
    742   COMMENT ON COLUMN merchant_statistic_bucket_amount.bucket_start IS 'start date for the bucket in seconds since the epoch';
    743   COMMENT ON COLUMN merchant_statistic_bucket_amount.bucket_range IS 'range of the bucket';
    744   COMMENT ON COLUMN merchant_statistic_bucket_amount.curr IS 'currency which this statistic is tracking the amount for';
    745   COMMENT ON COLUMN merchant_statistic_bucket_amount.cumulative_value IS 'amount in the respective currency, non-fractional amount value';
    746   COMMENT ON COLUMN merchant_statistic_bucket_amount.cumulative_frac IS 'amount in the respective currency, fraction in units of 1/100000000 of the base value';
    747 
    748   CREATE TABLE merchant_statistic_bucket_counter (
    749     bmeta_serial_id INT8 NOT NULL,
    750     bucket_start INT8 NOT NULL,
    751     bucket_range merchant.statistic_range NOT NULL,
    752     cumulative_number INT8 NOT NULL,
    753     PRIMARY KEY (bmeta_serial_id, bucket_start, bucket_range),
    754     CONSTRAINT merchant_statistic_bucket_counter_bmeta_serial_id_fkey
    755       FOREIGN KEY (bmeta_serial_id)
    756       REFERENCES merchant_statistic_bucket_meta(bmeta_serial_id) ON DELETE CASCADE
    757   );
    758   COMMENT ON TABLE merchant_statistic_bucket_counter IS 'various numeric statistics (cumulative counters) being tracked by bucket into which they fall';
    759   COMMENT ON COLUMN merchant_statistic_bucket_counter.bmeta_serial_id IS 'identifies what the statistic is about';
    760   COMMENT ON COLUMN merchant_statistic_bucket_counter.bucket_start IS 'start date for the bucket in seconds since the epoch';
    761   COMMENT ON COLUMN merchant_statistic_bucket_counter.bucket_range IS 'range of the bucket';
    762   COMMENT ON COLUMN merchant_statistic_bucket_counter.cumulative_number IS 'aggregate (sum) of tracked by the statistic; what exactly is tracked is determined by the keyword';
    763 
    764   CREATE TABLE merchant_statistic_counter_event (
    765     nevent_serial_id INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    766     imeta_serial_id INT8,
    767     slot INT8 NOT NULL,
    768     delta INT8 NOT NULL,
    769     UNIQUE (imeta_serial_id, slot),
    770     CONSTRAINT merchant_statistic_counter_event_imeta_serial_id_fkey
    771       FOREIGN KEY (imeta_serial_id)
    772       REFERENCES merchant_statistic_interval_meta(imeta_serial_id) ON DELETE CASCADE
    773   );
    774   COMMENT ON TABLE merchant_statistic_counter_event IS 'number to decrement an interval statistic by when a certain time value is reached';
    775   COMMENT ON COLUMN merchant_statistic_counter_event.nevent_serial_id IS 'unique identifier for this number event';
    776   COMMENT ON COLUMN merchant_statistic_counter_event.imeta_serial_id IS 'identifies what the statistic is about; must be of stype number';
    777   COMMENT ON COLUMN merchant_statistic_counter_event.slot IS 'identifies the time slot at which the given event(s) happened, rounded down by the respective precisions value';
    778   COMMENT ON COLUMN merchant_statistic_counter_event.delta IS 'total cumulative number that was added at the time identified by slot';
    779 
    780   CREATE TABLE merchant_template (
    781     template_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    782     template_id TEXT NOT NULL UNIQUE,
    783     template_description TEXT NOT NULL,
    784     otp_device_id INT8,
    785     template_contract JSONB NOT NULL,
    786     editable_defaults JSONB,
    787     CONSTRAINT merchant_template_otp_device_id_fkey
    788       FOREIGN KEY (otp_device_id)
    789       REFERENCES merchant_otp_devices(otp_serial) ON DELETE SET NULL
    790   );
    791   COMMENT ON TABLE merchant_template IS 'template used by the merchant (may be incomplete, frontend can override)';
    792   COMMENT ON COLUMN merchant_template.template_id IS 'slug identifying the templates in protocols (and on the QR code)';
    793   COMMENT ON COLUMN merchant_template.template_description IS 'Human-readable template description';
    794   COMMENT ON COLUMN merchant_template.otp_device_id IS 'OTP device ID used to verify payments by the shop using this QR code';
    795   COMMENT ON COLUMN merchant_template.template_contract IS 'The template contract will contains some additional information.';
    796   COMMENT ON COLUMN merchant_template.editable_defaults IS 'JSON object with fields matching the template contract, just with default values that are editable by the user';
    797 
    798   CREATE TABLE merchant_token_family_keys (
    799     token_family_key_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    800     token_family_serial INT8,
    801     pub BYTEA NOT NULL,
    802     h_pub BYTEA NOT NULL UNIQUE,
    803     priv BYTEA,
    804     cipher TEXT NOT NULL,
    805     signature_validity_start INT8 DEFAULT 0 NOT NULL,
    806     signature_validity_end INT8 DEFAULT 0 NOT NULL,
    807     private_key_deleted_at INT8 DEFAULT 0 NOT NULL,
    808     private_key_created_at INT8 DEFAULT 0 NOT NULL,
    809     CONSTRAINT h_pub_LENGTH_check CHECK ((LENGTH(h_pub) = 64)),
    810     CONSTRAINT merchant_token_family_keys_cipher_check CHECK ((cipher = ANY (ARRAY['rsa'::text, 'cs'::text]))),
    811     CONSTRAINT merchant_token_family_keys_token_family_serial_fkey
    812       FOREIGN KEY (token_family_serial)
    813       REFERENCES merchant_token_families(token_family_serial) ON DELETE CASCADE
    814   );
    815   COMMENT ON TABLE merchant_token_family_keys IS 'Keys for token families.';
    816   COMMENT ON COLUMN merchant_token_family_keys.token_family_serial IS 'Token family to which the key belongs.';
    817   COMMENT ON COLUMN merchant_token_family_keys.pub IS 'Public key of the token family.';
    818   COMMENT ON COLUMN merchant_token_family_keys.h_pub IS 'Hash of the public key for quick lookup.';
    819   COMMENT ON COLUMN merchant_token_family_keys.priv IS 'Private key of the token family; can be NULL if no more tokens of this family should be issued.';
    820   COMMENT ON COLUMN merchant_token_family_keys.cipher IS 'Cipher used (rsa or cs).';
    821   COMMENT ON COLUMN merchant_token_family_keys.signature_validity_start IS 'Specifies the earliest time at which tokens signed with this key can be considered valid. Allows tokens to be issued way in advance of their validity.';
    822   COMMENT ON COLUMN merchant_token_family_keys.signature_validity_end IS 'Specifies when the tokens signed by this key expire.';
    823   COMMENT ON COLUMN merchant_token_family_keys.private_key_deleted_at IS 'Specifies how long tokens signed by this key can be created, that is the point at which the private key may be deleted. Computed by determining when the *next* validity period starts, or when the overall token family validity period ends.';
    824   COMMENT ON COLUMN merchant_token_family_keys.private_key_created_at IS 'Specifies when the private key was created. Not terribly useful, mostly for debugging.';
    825 
    826   CREATE TABLE merchant_transfers (
    827     credit_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    828     exchange_url TEXT NOT NULL,
    829     wtid BYTEA,
    830     credit_amount merchant.taler_amount_currency NOT NULL,
    831     account_serial INT8 NOT NULL,
    832     bank_serial_id INT8,
    833     expected BOOLEAN DEFAULT FALSE,
    834     execution_time INT8 DEFAULT 0,
    835     CONSTRAINT merchant_transfers_wtid_check CHECK ((LENGTH(wtid) = 32)),
    836     CONSTRAINT merchant_transfers_unique UNIQUE (wtid, exchange_url, account_serial, bank_serial_id),
    837     CONSTRAINT merchant_transfers_account_serial_fkey
    838       FOREIGN KEY (account_serial)
    839       REFERENCES merchant_accounts(account_serial) ON DELETE CASCADE
    840   );
    841   COMMENT ON TABLE merchant_transfers IS 'table represents confirmed incoming wire transfers';
    842   COMMENT ON COLUMN merchant_transfers.credit_serial IS 'Unique identifier for this wire transfer in this backend';
    843   COMMENT ON COLUMN merchant_transfers.exchange_url IS 'Base URL of the exchange that originated the wire transfer as extracted from the wire transfer subject';
    844   COMMENT ON COLUMN merchant_transfers.wtid IS 'Unique wire transfer identifier (or at least, should be unique by protocol) as selected by the exchange and extracted from the wire transfer subject';
    845   COMMENT ON COLUMN merchant_transfers.credit_amount IS 'actual value of the confirmed wire transfer';
    846   COMMENT ON COLUMN merchant_transfers.account_serial IS 'Merchant bank account that received this wire transfer; also implies the merchant instance implicated by the wire transfer';
    847   COMMENT ON COLUMN merchant_transfers.bank_serial_id IS 'Row ID of the wire transfer from the automated import; NULL if not available (like when a human manually imported the transfer)';
    848   COMMENT ON COLUMN merchant_transfers.expected IS 'True if this wire transfer was expected (has matching entry in merchant_expected_transfers); set automatically via INSERT TRIGGER merchant_transfers_insert_trigger';
    849   COMMENT ON COLUMN merchant_transfers.execution_time IS 'Time when the merchant transfer was added and thus roughly received in our bank account';
    850 
    851   -- ===================================================================
    852   -- Tier 2: tables referencing Tier 1 (and earlier) tables.
    853   -- ===================================================================
    854 
    855   CREATE TABLE merchant_deposits (
    856     deposit_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    857     coin_offset INT4 NOT NULL,
    858     deposit_confirmation_serial INT8 NOT NULL,
    859     coin_pub BYTEA NOT NULL,
    860     coin_sig BYTEA NOT NULL,
    861     amount_with_fee merchant.taler_amount_currency NOT NULL,
    862     deposit_fee merchant.taler_amount_currency NOT NULL,
    863     refund_fee merchant.taler_amount_currency NOT NULL,
    864     settlement_retry_needed BOOLEAN DEFAULT TRUE,
    865     settlement_retry_time INT8 DEFAULT 0,
    866     settlement_last_http_status INT4,
    867     settlement_last_ec INT4,
    868     settlement_last_detail TEXT,
    869     settlement_wtid BYTEA,
    870     settlement_coin_contribution merchant.taler_amount_currency,
    871     settlement_expected_credit_serial INT8,
    872     signkey_serial INT8,
    873     settlement_exchange_sig BYTEA,
    874     CONSTRAINT merchant_deposits_coin_pub_check CHECK ((LENGTH(coin_pub) = 32)),
    875     CONSTRAINT merchant_deposits_coin_sig_check CHECK ((LENGTH(coin_sig) = 64)),
    876     CONSTRAINT merchant_deposits_settlement_exchange_sig_check CHECK ((LENGTH(settlement_exchange_sig) = 64)),
    877     CONSTRAINT merchant_deposits_settlement_wtid_check CHECK ((LENGTH(settlement_wtid) = 32)),
    878     UNIQUE (deposit_confirmation_serial, coin_pub),
    879     CONSTRAINT merchant_deposits_deposit_confirmation_serial_fkey
    880       FOREIGN KEY (deposit_confirmation_serial)
    881       REFERENCES merchant_deposit_confirmations(deposit_confirmation_serial) ON DELETE CASCADE,
    882     CONSTRAINT merchant_deposits_settlement_expected_credit_serial_fkey
    883       FOREIGN KEY (settlement_expected_credit_serial)
    884       REFERENCES merchant_expected_transfers(expected_credit_serial),
    885     CONSTRAINT merchant_deposits_signkey_serial_fkey
    886       FOREIGN KEY (signkey_serial)
    887       REFERENCES merchant.merchant_exchange_signing_keys(signkey_serial) ON DELETE CASCADE
    888   );
    889   COMMENT ON TABLE merchant_deposits IS 'Table with the deposit details for each coin we deposited at the exchange';
    890   COMMENT ON COLUMN merchant_deposits.coin_offset IS 'Offset of this coin in the batch';
    891   COMMENT ON COLUMN merchant_deposits.deposit_confirmation_serial IS 'Reference to the deposit confirmation of the exchange';
    892   COMMENT ON COLUMN merchant_deposits.coin_pub IS 'Public key of the coin that was deposited';
    893   COMMENT ON COLUMN merchant_deposits.coin_sig IS 'Signature by the coin over the deposit request';
    894   COMMENT ON COLUMN merchant_deposits.amount_with_fee IS 'Total amount (incl. fee) of the coin that was deposited';
    895   COMMENT ON COLUMN merchant_deposits.deposit_fee IS 'Deposit fee (for this coin) that was paid';
    896   COMMENT ON COLUMN merchant_deposits.refund_fee IS 'How high would the refund fee be (for this coin)';
    897   COMMENT ON COLUMN merchant_deposits.settlement_retry_needed IS 'True if we should ask the exchange in the future about the settlement';
    898   COMMENT ON COLUMN merchant_deposits.settlement_retry_time IS 'When should we next ask the exchange about the settlement wire transfer for this coin, initially set to the wire transfer deadline plus a bit of slack';
    899   COMMENT ON COLUMN merchant_deposits.settlement_last_http_status IS 'HTTP status of our last inquiry with the exchange for this deposit, NULL if we never inquired, 0 on timeout';
    900   COMMENT ON COLUMN merchant_deposits.settlement_last_ec IS 'Taler error code for our last inquiry with the exchange for this deposit, NULL if we never inquired, 0 on success';
    901   COMMENT ON COLUMN merchant_deposits.settlement_last_detail IS 'Taler error detail for our last inquiry with the exchange for this deposit, NULL if we never inquired or on success';
    902   COMMENT ON COLUMN merchant_deposits.settlement_wtid IS 'Wire transfer identifier of the transfer of the exchange to this merchant settling the payment';
    903   COMMENT ON COLUMN merchant_deposits.settlement_coin_contribution IS 'Contribution of this coin to the overall wire transfer made by the exchange as claimed by exchange_sig; should match amount_with_fee minus deposit_fee, NULL if we did not get a reply from the exchange';
    904   COMMENT ON COLUMN merchant_deposits.settlement_expected_credit_serial IS 'Identifies the expected wire transfer from the exchange to the merchant that settled the deposit of coin, NULL if unknown';
    905   COMMENT ON COLUMN merchant_deposits.signkey_serial IS 'Identifies the online signing key of the exchange used to make the exchange_sig, NULL for none';
    906   COMMENT ON COLUMN merchant_deposits.settlement_exchange_sig IS 'Exchange signature of purpose TALER_SIGNATURE_EXCHANGE_CONFIRM_WIRE, NULL if we did not get such an exchange signature';
    907   CREATE INDEX merchant_deposits_by_deposit_confirmation
    908     ON merchant_deposits (deposit_confirmation_serial);
    909   CREATE INDEX merchant_deposits_by_settlement_open
    910     ON merchant_deposits (settlement_retry_time)
    911     WHERE settlement_retry_needed;
    912   COMMENT ON INDEX merchant_deposits_by_settlement_open IS 'For select_open_deposit_settlements';
    913 
    914   CREATE TABLE merchant_inventory_locks (
    915     product_serial INT8 NOT NULL,
    916     lock_uuid BYTEA NOT NULL,
    917     total_locked INT8 NOT NULL,
    918     expiration INT8 NOT NULL,
    919     total_locked_frac INT4 DEFAULT 0 NOT NULL,
    920     CONSTRAINT merchant_inventory_locks_lock_uuid_check CHECK ((LENGTH(lock_uuid) = 16)),
    921     CONSTRAINT merchant_inventory_locks_product_serial_fkey
    922       FOREIGN KEY (product_serial)
    923       REFERENCES merchant_inventory(product_serial) ON DELETE CASCADE
    924   );
    925   COMMENT ON TABLE merchant_inventory_locks IS 'locks on inventory helt by shopping carts; note that locks MAY not be honored if merchants increase total_lost for inventory';
    926   COMMENT ON COLUMN merchant_inventory_locks.product_serial IS 'identifies the inventory product being locked';
    927   COMMENT ON COLUMN merchant_inventory_locks.lock_uuid IS 'unique identifier for the lock';
    928   COMMENT ON COLUMN merchant_inventory_locks.total_locked IS 'how many units of the product does this lock reserve';
    929   COMMENT ON COLUMN merchant_inventory_locks.expiration IS 'when does this lock automatically expire (if no order is created)';
    930   COMMENT ON COLUMN merchant_inventory_locks.total_locked_frac IS 'Fractional part of locked stock in units of 1/1000000 of the base value';
    931   CREATE INDEX merchant_inventory_locks_by_expiration
    932     ON merchant_inventory_locks (expiration);
    933   CREATE INDEX merchant_inventory_locks_by_uuid
    934     ON merchant_inventory_locks (lock_uuid);
    935 
    936   CREATE TABLE merchant_issued_tokens (
    937     issued_token_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    938     h_contract_terms BYTEA NOT NULL,
    939     token_family_key_serial INT8,
    940     blind_sig BYTEA NOT NULL,
    941     CONSTRAINT merchant_issued_tokens_h_contract_terms_check CHECK ((LENGTH(h_contract_terms) = 64)),
    942     CONSTRAINT merchant_issued_tokens_token_family_key_serial_fkey
    943       FOREIGN KEY (token_family_key_serial)
    944       REFERENCES merchant_token_family_keys(token_family_key_serial) ON DELETE CASCADE
    945   );
    946   COMMENT ON TABLE merchant_issued_tokens IS 'Tokens that have been (blindly) issued to customers.';
    947   COMMENT ON COLUMN merchant_issued_tokens.h_contract_terms IS 'This is no foreign key by design.';
    948   COMMENT ON COLUMN merchant_issued_tokens.token_family_key_serial IS 'Token family key to which the spent token belongs.';
    949   COMMENT ON COLUMN merchant_issued_tokens.blind_sig IS 'Blind signature made with token issue key to prove validity of token.';
    950 
    951   CREATE TABLE merchant_order_locks (
    952     product_serial INT8 NOT NULL,
    953     total_locked INT8 NOT NULL,
    954     order_serial INT8 NOT NULL,
    955     total_locked_frac INT4 DEFAULT 0 NOT NULL,
    956     CONSTRAINT merchant_order_locks_order_serial_fkey
    957       FOREIGN KEY (order_serial)
    958       REFERENCES merchant_orders(order_serial) ON DELETE CASCADE,
    959     CONSTRAINT merchant_order_locks_product_serial_fkey
    960       FOREIGN KEY (product_serial)
    961       REFERENCES merchant_inventory(product_serial)
    962   );
    963   COMMENT ON TABLE merchant_order_locks IS 'locks on orders awaiting claim and payment; note that locks MAY not be honored if merchants increase total_lost for inventory';
    964   COMMENT ON COLUMN merchant_order_locks.total_locked IS 'how many units of the product does this lock reserve';
    965   COMMENT ON COLUMN merchant_order_locks.order_serial IS 'order for which the lock applies';
    966   COMMENT ON COLUMN merchant_order_locks.total_locked_frac IS 'Fractional part of locked stock associated with orders in units of 1/1000000 of the base value';
    967   CREATE INDEX merchant_orders_locks_by_order_and_product
    968     ON merchant_order_locks (order_serial, product_serial);
    969 
    970   CREATE TABLE merchant_product_categories (
    971     category_serial INT8 NOT NULL,
    972     product_serial INT8 NOT NULL,
    973     CONSTRAINT merchant_product_categories_category_serial_fkey
    974       FOREIGN KEY (category_serial)
    975       REFERENCES merchant_categories(category_serial) ON DELETE CASCADE,
    976     CONSTRAINT merchant_product_categories_product_serial_fkey
    977       FOREIGN KEY (product_serial)
    978       REFERENCES merchant_inventory(product_serial) ON DELETE CASCADE
    979   );
    980   COMMENT ON TABLE merchant_product_categories IS 'N:M map from products to categories (a product can be in any number of categories, including zero)';
    981   COMMENT ON COLUMN merchant_product_categories.category_serial IS 'Reference to a category the product is part of';
    982   COMMENT ON COLUMN merchant_product_categories.product_serial IS 'Reference to a product which is in the given category';
    983   CREATE INDEX merchant_categories_by_category
    984     ON merchant_product_categories (category_serial);
    985   CREATE INDEX merchant_categories_by_product
    986     ON merchant_product_categories (product_serial);
    987 
    988   CREATE TABLE merchant_refund_proofs (
    989     refund_serial INT8 NOT NULL PRIMARY KEY,
    990     exchange_sig BYTEA NOT NULL,
    991     signkey_serial INT8 NOT NULL,
    992     CONSTRAINT merchant_refund_proofs_exchange_sig_check CHECK ((LENGTH(exchange_sig) = 64)),
    993     CONSTRAINT merchant_refund_proofs_refund_serial_fkey
    994       FOREIGN KEY (refund_serial)
    995       REFERENCES merchant_refunds(refund_serial) ON DELETE CASCADE,
    996     CONSTRAINT merchant_refund_proofs_signkey_serial_fkey
    997       FOREIGN KEY (signkey_serial)
    998       REFERENCES merchant.merchant_exchange_signing_keys(signkey_serial) ON DELETE CASCADE
    999   );
   1000   COMMENT ON TABLE merchant_refund_proofs IS 'Refunds confirmed by the exchange (not all approved refunds are grabbed by the wallet)';
   1001   COMMENT ON COLUMN merchant_refund_proofs.refund_serial IS 'refund process to which this proof applies';
   1002   COMMENT ON COLUMN merchant_refund_proofs.exchange_sig IS 'signature by the exchange confirming the refund';
   1003   COMMENT ON COLUMN merchant_refund_proofs.signkey_serial IS 'reference to the public key of the exchange by which exchange_sig was made';
   1004 
   1005   CREATE TABLE merchant_used_tokens (
   1006     spent_token_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
   1007     h_contract_terms BYTEA NOT NULL,
   1008     token_family_key_serial INT8,
   1009     token_pub BYTEA NOT NULL UNIQUE,
   1010     token_sig BYTEA NOT NULL,
   1011     blind_sig BYTEA NOT NULL,
   1012     CONSTRAINT merchant_spent_tokens_h_contract_terms_check CHECK ((LENGTH(h_contract_terms) = 64)),
   1013     CONSTRAINT merchant_spent_tokens_token_pub_check CHECK ((LENGTH(token_pub) = 32)),
   1014     CONSTRAINT merchant_spent_tokens_token_sig_check CHECK ((LENGTH(token_sig) = 64)),
   1015     CONSTRAINT merchant_spent_tokens_token_family_key_serial_fkey
   1016       FOREIGN KEY (token_family_key_serial)
   1017       REFERENCES merchant_token_family_keys(token_family_key_serial) ON DELETE CASCADE
   1018   );
   1019   COMMENT ON TABLE merchant_used_tokens IS 'Tokens that have been spent by customers.';
   1020   COMMENT ON COLUMN merchant_used_tokens.h_contract_terms IS 'This is no foreign key by design.';
   1021   COMMENT ON COLUMN merchant_used_tokens.token_family_key_serial IS 'Token family to which the spent token belongs.';
   1022   COMMENT ON COLUMN merchant_used_tokens.token_pub IS 'Public key of the spent token.';
   1023   COMMENT ON COLUMN merchant_used_tokens.token_sig IS 'Signature that the token was spent on specified order.';
   1024   COMMENT ON COLUMN merchant_used_tokens.blind_sig IS 'Blind signature for the spent token to prove validity of token.';
   1025 
   1026   CREATE TABLE merchant_statistic_interval_amount (
   1027     imeta_serial_id INT8 NOT NULL,
   1028     event_delimiter INT8 NOT NULL,
   1029     range INT8 NOT NULL,
   1030     curr character varying(12) NOT NULL,
   1031     cumulative_value INT8 NOT NULL,
   1032     cumulative_frac INT4 NOT NULL,
   1033     PRIMARY KEY (imeta_serial_id, curr, range),
   1034     CONSTRAINT merchant_statistic_interval_amount_event_delimiter_fkey
   1035       FOREIGN KEY (event_delimiter)
   1036       REFERENCES merchant_statistic_amount_event(aevent_serial_id) ON DELETE RESTRICT,
   1037     CONSTRAINT merchant_statistic_interval_amount_imeta_serial_id_fkey
   1038       FOREIGN KEY (imeta_serial_id)
   1039       REFERENCES merchant_statistic_interval_meta(imeta_serial_id) ON DELETE CASCADE
   1040   );
   1041   COMMENT ON TABLE merchant_statistic_interval_amount IS 'various amount statistics (in various currencies) being tracked';
   1042   COMMENT ON COLUMN merchant_statistic_interval_amount.imeta_serial_id IS 'identifies what the statistic is about';
   1043   COMMENT ON COLUMN merchant_statistic_interval_amount.event_delimiter IS 'FIXME';
   1044   COMMENT ON COLUMN merchant_statistic_interval_amount.range IS 'for which range is this the counter; note that the cumulative_number excludes the values already stored in smaller ranges';
   1045   COMMENT ON COLUMN merchant_statistic_interval_amount.curr IS 'currency which this statistic is tracking the amount for';
   1046   COMMENT ON COLUMN merchant_statistic_interval_amount.cumulative_value IS 'amount in the respective currency, non-fractional amount value';
   1047   COMMENT ON COLUMN merchant_statistic_interval_amount.cumulative_frac IS 'amount in the respective currency, fraction in units of 1/100000000 of the base value';
   1048 
   1049   CREATE TABLE merchant_statistic_interval_counter (
   1050     imeta_serial_id INT8 NOT NULL,
   1051     range INT8 NOT NULL,
   1052     event_delimiter INT8 NOT NULL,
   1053     cumulative_number INT8 NOT NULL,
   1054     PRIMARY KEY (imeta_serial_id, range),
   1055     CONSTRAINT merchant_statistic_interval_counter_event_delimiter_fkey
   1056       FOREIGN KEY (event_delimiter)
   1057       REFERENCES merchant_statistic_counter_event(nevent_serial_id) ON DELETE RESTRICT,
   1058     CONSTRAINT merchant_statistic_interval_counter_imeta_serial_id_fkey
   1059       FOREIGN KEY (imeta_serial_id)
   1060       REFERENCES merchant_statistic_interval_meta(imeta_serial_id) ON DELETE CASCADE
   1061   );
   1062   COMMENT ON TABLE merchant_statistic_interval_counter IS 'various numeric statistics (cumulative counters) being tracked';
   1063   COMMENT ON COLUMN merchant_statistic_interval_counter.imeta_serial_id IS 'identifies what the statistic is about';
   1064   COMMENT ON COLUMN merchant_statistic_interval_counter.range IS 'for which range is this the counter; note that the cumulative_number excludes the values already stored in smaller ranges';
   1065   COMMENT ON COLUMN merchant_statistic_interval_counter.event_delimiter IS 'determines the last event currently included in the interval';
   1066   COMMENT ON COLUMN merchant_statistic_interval_counter.cumulative_number IS 'aggregate (sum) of tracked by the statistic; what exactly is tracked is determined by the keyword';
   1067 
   1068   CREATE TABLE merchant_transfer_signatures (
   1069     expected_credit_serial INT8 NOT NULL PRIMARY KEY,
   1070     signkey_serial INT8 NOT NULL,
   1071     wire_fee merchant.taler_amount_currency NOT NULL,
   1072     credit_amount merchant.taler_amount_currency NOT NULL,
   1073     execution_time INT8 NOT NULL,
   1074     exchange_sig BYTEA NOT NULL,
   1075     CONSTRAINT merchant_transfer_signatures_exchange_sig_check CHECK ((LENGTH(exchange_sig) = 64)),
   1076     CONSTRAINT merchant_transfer_signatures_expected_credit_serial_fkey
   1077       FOREIGN KEY (expected_credit_serial)
   1078       REFERENCES merchant_expected_transfers(expected_credit_serial) ON DELETE CASCADE,
   1079     CONSTRAINT merchant_transfer_signatures_signkey_serial_fkey
   1080       FOREIGN KEY (signkey_serial)
   1081       REFERENCES merchant.merchant_exchange_signing_keys(signkey_serial) ON DELETE CASCADE
   1082   );
   1083   COMMENT ON TABLE merchant_transfer_signatures IS 'table represents the main information returned from the /transfer request to the exchange.';
   1084   COMMENT ON COLUMN merchant_transfer_signatures.expected_credit_serial IS 'expected wire transfer this signature is about';
   1085   COMMENT ON COLUMN merchant_transfer_signatures.signkey_serial IS 'Online signing key by the exchange that was used for the exchange_sig signature';
   1086   COMMENT ON COLUMN merchant_transfer_signatures.wire_fee IS 'wire fee charged by the exchange for this transfer';
   1087   COMMENT ON COLUMN merchant_transfer_signatures.credit_amount IS 'actual value of the (aggregated) wire transfer, excluding the wire fee, according to the exchange';
   1088   COMMENT ON COLUMN merchant_transfer_signatures.execution_time IS 'Execution time as claimed by the exchange, roughly matches time seen by merchant';
   1089   COMMENT ON COLUMN merchant_transfer_signatures.exchange_sig IS 'signature by the exchange of purpose TALER_SIGNATURE_EXCHANGE_CONFIRM_WIRE_DEPOSIT';
   1090 
   1091   -- ===================================================================
   1092   -- Tier 3: tables referencing Tier 2 tables.
   1093   -- ===================================================================
   1094 
   1095   CREATE TABLE merchant_expected_transfer_to_coin (
   1096     deposit_serial INT8 NOT NULL UNIQUE,
   1097     expected_credit_serial INT8 NOT NULL,
   1098     offset_in_exchange_list INT8 NOT NULL,
   1099     exchange_deposit_value merchant.taler_amount_currency NOT NULL,
   1100     exchange_deposit_fee merchant.taler_amount_currency NOT NULL,
   1101     CONSTRAINT merchant_expected_transfer_to_coin_deposit_serial_fkey
   1102       FOREIGN KEY (deposit_serial)
   1103       REFERENCES merchant_deposits(deposit_serial) ON DELETE CASCADE,
   1104     CONSTRAINT merchant_expected_transfer_to_coin_expected_credit_serial_fkey
   1105       FOREIGN KEY (expected_credit_serial)
   1106       REFERENCES merchant_expected_transfers(expected_credit_serial) ON DELETE CASCADE
   1107   );
   1108   COMMENT ON TABLE merchant_expected_transfer_to_coin IS 'Mapping of (credit) transfers to (deposited) coins';
   1109   COMMENT ON COLUMN merchant_expected_transfer_to_coin.deposit_serial IS 'Identifies the deposited coin that the wire transfer presumably settles';
   1110   COMMENT ON COLUMN merchant_expected_transfer_to_coin.expected_credit_serial IS 'Identifies the expected wire transfer that settles the given deposited coin';
   1111   COMMENT ON COLUMN merchant_expected_transfer_to_coin.offset_in_exchange_list IS 'The exchange settlement data includes an array of the settled coins; this is the index of the coin in that list, useful to reconstruct the correct sequence of coins as needed to check the exchange signature';
   1112   COMMENT ON COLUMN merchant_expected_transfer_to_coin.exchange_deposit_value IS 'Deposit value as claimed by the exchange, should match our values in merchant_deposits minus refunds';
   1113   COMMENT ON COLUMN merchant_expected_transfer_to_coin.exchange_deposit_fee IS 'Deposit value as claimed by the exchange, should match our values in merchant_deposits';
   1114   CREATE INDEX merchant_transfers_by_credit
   1115     ON merchant_expected_transfer_to_coin (expected_credit_serial);
   1116 
   1117   SET LOCAL search_path TO merchant;
   1118 END
   1119 $OUTER$;
   1120 
   1121 INSERT INTO merchant.instance_fixups
   1122   (migration_name
   1123   ,version)
   1124   VALUES
   1125   ('merchant_0036_init'
   1126   ,36);