BEGIN;
SET search_path TO merchant;
DROP FUNCTION IF EXISTS merchant_insert_deposit_to_transfer;
CREATE FUNCTION merchant_insert_deposit_to_transfer (
  IN in_deposit_serial INT8,
  IN in_coin_contribution taler_amount_currency,
  IN in_execution_time INT8,
  IN in_exchange_url TEXT,
  IN in_h_wire BYTEA,
  IN in_exchange_sig BYTEA,
  IN in_exchange_pub BYTEA,
  IN in_wtid BYTEA,
  OUT out_dummy BOOL)
LANGUAGE plpgsql
AS $$
DECLARE
  my_signkey_serial INT8;
  my_account_serial INT8;
  my_decose INT8;
  my_order_serial INT8;
  my_expected_credit_serial INT8;
  my_wire_pending_cleared BOOL;
  my_order_id TEXT;
BEGIN
  out_dummy=FALSE;
SELECT signkey_serial
  INTO my_signkey_serial
  FROM merchant_exchange_signing_keys
 WHERE exchange_pub=in_exchange_pub
   ORDER BY start_date DESC
   LIMIT 1;
IF NOT FOUND
THEN
  UPDATE merchant_deposits
     SET settlement_last_ec=2029 
        ,settlement_last_http_status=200
        ,settlement_last_detail=ENCODE(in_exchange_pub, 'hex')
        ,settlement_wtid=in_wtid
        ,settlement_retry_needed=TRUE
        ,settlement_retry_time=(EXTRACT(epoch FROM (CURRENT_TIME + interval '8 hours')) * 1000000)::INT8
   WHERE deposit_serial=in_deposit_serial;
  RETURN;
END IF;
SELECT deposit_confirmation_serial
  INTO my_decose
  FROM merchant_deposits
 WHERE deposit_serial=in_deposit_serial;
SELECT account_serial
  INTO my_account_serial
  FROM merchant_deposit_confirmations mdc
  JOIN merchant_accounts ma
    USING (account_serial)
 WHERE mdc.deposit_confirmation_serial=my_decose
   AND ma.h_wire=in_h_wire;
IF NOT FOUND
THEN
  UPDATE merchant_deposits
     SET settlement_last_ec=2558 
        ,settlement_last_http_status=200
        ,settlement_last_detail=ENCODE(in_h_wire, 'hex')
        ,settlement_wtid=in_wtid
        ,settlement_retry_needed=FALSE
        ,settlement_coin_contribution=in_coin_contribution
        ,signkey_serial=my_signkey_serial
        ,settlement_exchange_sig=in_exchange_sig
   WHERE deposit_serial=in_deposit_serial;
  RETURN;
END IF;
SELECT expected_credit_serial
  INTO my_expected_credit_serial
  FROM merchant_expected_transfers
  WHERE wtid=in_wtid
    AND exchange_url=in_exchange_url
    AND account_serial=my_account_serial;
IF NOT FOUND
THEN
  INSERT INTO merchant_expected_transfers
    (exchange_url
    ,wtid
    ,account_serial
    ,expected_time)
   VALUES
    (in_exchange_url
    ,in_wtid
    ,my_account_serial
    ,in_execution_time)
   RETURNING expected_credit_serial
     INTO my_expected_credit_serial;
END IF;
UPDATE merchant_deposits
   SET settlement_last_ec=0
      ,settlement_last_http_status=200
      ,settlement_last_detail=NULL
      ,settlement_wtid=in_wtid
      ,settlement_retry_needed=FALSE
      ,settlement_coin_contribution=in_coin_contribution
      ,settlement_expected_credit_serial=my_expected_credit_serial
      ,signkey_serial=my_signkey_serial
      ,settlement_exchange_sig=in_exchange_sig
 WHERE deposit_serial=in_deposit_serial;
NOTIFY XR6849FMRD2AJFY1E2YY0GWA8GN0YT407Z66WHJB0SAKJWF8G2Q60;
END $$;
DROP FUNCTION IF EXISTS merchant_do_insert_product;
CREATE FUNCTION merchant_do_insert_product (
  IN in_instance_id TEXT,
  IN in_product_id TEXT,
  IN in_description TEXT,
  IN in_description_i18n JSONB,
  IN in_unit TEXT,
  IN in_image TEXT,
  IN in_taxes JSONB,
  IN in_price taler_amount_currency,
  IN ina_price_list taler_amount_currency[],
  IN in_total_stock INT8,
  IN in_total_stock_frac INT4,
  IN in_allow_fractional_quantity BOOL,
  IN in_fractional_precision_level INT4,
  IN in_address JSONB,
  IN in_next_restock INT8,
  IN in_minimum_age INT4,
  IN ina_categories INT8[],
  IN in_product_name TEXT,
  OUT out_no_instance BOOL,
  OUT out_conflict BOOL,
  OUT out_no_cat INT8)
LANGUAGE plpgsql
AS $$
DECLARE
  my_merchant_id INT8;
  my_product_serial INT8;
  i INT8;
  ini_cat INT8;
  my_price taler_amount_currency;
  my_price_array taler_amount_currency[];
BEGIN
SELECT merchant_serial
  INTO my_merchant_id
  FROM merchant_instances
 WHERE merchant_id=in_instance_id;
IF NOT FOUND
THEN
  out_no_instance=TRUE;
  out_conflict=FALSE;
  out_no_cat=NULL;
  RETURN;
END IF;
out_no_instance=FALSE;
IF COALESCE (array_length(ina_price_list,1),0) = 0
THEN
  my_price_array := ARRAY[in_price]::taler_amount_currency[];
ELSE
  my_price_array := ina_price_list;
END IF;
my_price := my_price_array[1];
INSERT INTO merchant_inventory
 (merchant_serial
 ,product_id
 ,product_name
 ,description
 ,description_i18n
 ,unit
 ,image
 ,image_hash
 ,taxes
 ,price
 ,price_array
 ,total_stock
 ,total_stock_frac
 ,allow_fractional_quantity
 ,fractional_precision_level
 ,address
 ,next_restock
  ,minimum_age
) VALUES (
  my_merchant_id
 ,in_product_id
 ,in_product_name
 ,in_description
 ,in_description_i18n
 ,in_unit
 ,in_image
 ,CASE
    WHEN (in_image IS NULL) OR (in_image = '')
    THEN NULL
    ELSE encode(public.digest(convert_to(in_image, 'UTF8'),
                       'sha256'),
                'hex')
  END
 ,in_taxes
 ,my_price
 ,my_price_array
 ,in_total_stock
 ,in_total_stock_frac
 ,in_allow_fractional_quantity
 ,in_fractional_precision_level
 ,in_address
 ,in_next_restock
 ,in_minimum_age)
ON CONFLICT (merchant_serial, product_id) DO NOTHING
 RETURNING product_serial
 INTO my_product_serial;
IF NOT FOUND
THEN
  SELECT product_serial
    INTO my_product_serial
    FROM merchant_inventory
  WHERE merchant_serial=my_merchant_id
    AND product_id=in_product_id
    AND product_name=in_product_name
    AND description=in_description
    AND description_i18n=in_description_i18n
    AND unit=in_unit
    AND image=in_image
    AND taxes=in_taxes
    AND price=my_price
    AND to_jsonb(COALESCE(price_array, ARRAY[]::taler_amount_currency[]))
        = to_jsonb(COALESCE(my_price_array, ARRAY[]::taler_amount_currency[]))
    AND total_stock=in_total_stock
    AND total_stock_frac=in_total_stock_frac
    AND allow_fractional_quantity=in_allow_fractional_quantity
    AND fractional_precision_level=in_fractional_precision_level
    AND address=in_address
    AND next_restock=in_next_restock
    AND minimum_age=in_minimum_age;
  IF NOT FOUND
  THEN
    out_conflict=TRUE;
    out_no_cat=NULL;
    RETURN;
  END IF;
  FOR i IN 1..COALESCE(array_length(ina_categories,1),0)
  LOOP
    ini_cat=ina_categories[i];
    PERFORM
      FROM merchant_product_categories
      WHERE product_serial=my_product_serial
        AND category_serial=ini_cat;
    IF NOT FOUND
    THEN
      out_conflict=TRUE;
      out_no_cat=NULL;
      RETURN;
    END IF;
  END LOOP;
  SELECT COUNT(*)
    INTO i
    FROM merchant_product_categories
    WHERE product_serial=my_product_serial;
  IF i != array_length(ina_categories,1)
  THEN
    out_conflict=TRUE;
    out_no_cat=NULL;
    RETURN;
  END IF;
  out_conflict=FALSE;
  out_no_cat=NULL;
  RETURN;
END IF;
out_conflict=FALSE;
FOR i IN 1..COALESCE(array_length(ina_categories,1),0)
LOOP
  ini_cat=ina_categories[i];
  INSERT INTO merchant_product_categories
   (product_serial
   ,category_serial)
  VALUES
   (my_product_serial
   ,ini_cat)
  ON CONFLICT DO NOTHING;
  IF NOT FOUND
  THEN
    out_no_cat=i;
    RETURN;
  END IF;
END LOOP;
out_no_cat=NULL;
END $$;
DROP FUNCTION IF EXISTS merchant_do_insert_issued_token;
CREATE FUNCTION merchant_do_insert_issued_token (
  IN in_h_issue_pub BYTEA,
  IN in_h_contract_terms BYTEA,
  IN in_blind_sig BYTEA,
  OUT out_no_family BOOL,
  OUT out_existed BOOL)
LANGUAGE plpgsql
AS $$
DECLARE
  my_rec RECORD;
  my_tfk_serial INT8;
  my_tf_serial INT8;
BEGIN
SELECT token_family_key_serial
      ,token_family_serial
  INTO my_rec
  FROM merchant_token_family_keys
 WHERE h_pub = in_h_issue_pub;
IF NOT FOUND
THEN
  out_no_family = TRUE;
  out_existed = FALSE;
  return;
END IF;
my_tfk_serial = my_rec.token_family_key_serial;
my_tf_serial = my_rec.token_family_serial;
out_no_family = FALSE;
INSERT INTO merchant_issued_tokens
  (token_family_key_serial
  ,h_contract_terms
  ,blind_sig
  ) VALUES
  (my_tfk_serial
  ,in_h_contract_terms
  ,in_blind_sig)
  ON CONFLICT DO NOTHING;
IF NOT FOUND
THEN
  out_existed = TRUE;
  return;
END IF;
out_existed = FALSE;
UPDATE merchant_token_families
   SET issued=issued+1
  WHERE token_family_serial=my_tf_serial;
END $$;
DROP FUNCTION IF EXISTS merchant_do_insert_spent_token;
CREATE FUNCTION merchant_do_insert_spent_token (
  IN in_h_contract_terms BYTEA,
  IN in_h_issue_pub BYTEA,
  IN in_use_pub BYTEA,
  IN in_use_sig BYTEA,
  IN in_issue_sig BYTEA,
  OUT out_no_family BOOL,
  OUT out_conflict BOOL)
LANGUAGE plpgsql
AS $$
DECLARE
  my_rec RECORD;
  my_tfk_serial INT8;
  my_tf_serial INT8;
BEGIN
SELECT token_family_key_serial
      ,token_family_serial
  INTO my_rec
  FROM merchant_token_family_keys
 WHERE h_pub = in_h_issue_pub;
IF NOT FOUND
THEN
  out_no_family = TRUE;
  out_conflict = FALSE;
  return;
END IF;
out_no_family = FALSE;
my_tfk_serial = my_rec.token_family_key_serial;
my_tf_serial = my_rec.token_family_serial;
INSERT INTO merchant_used_tokens
  (token_family_key_serial
  ,h_contract_terms
  ,token_pub
  ,token_sig
  ,blind_sig
  ) VALUES
  (my_tfk_serial
  ,in_h_contract_terms
  ,in_use_pub
  ,in_use_sig
  ,in_issue_sig)
  ON CONFLICT DO NOTHING;
IF NOT FOUND
THEN
  PERFORM FROM merchant_used_tokens
    WHERE token_family_key_serial=my_tfk_serial
      AND h_contract_terms=in_h_contract_terms
      AND token_pub=in_use_pub
      AND token_sig=in_use_sig
      AND blind_sig=in_issue_sig;
  out_conflict = NOT FOUND;
  return;
END IF;
out_conflict = FALSE;
UPDATE merchant_token_families
   SET used=used+1
 WHERE token_family_serial=my_tf_serial;
END $$;
DROP FUNCTION IF EXISTS merchant_do_insert_transfer_details;
CREATE FUNCTION merchant_do_insert_transfer_details (
  IN in_instance_id TEXT,
  IN in_exchange_url TEXT,
  IN in_payto_uri TEXT,
  IN in_wtid BYTEA,
  IN in_execution_time INT8,
  IN in_exchange_pub BYTEA,
  IN in_exchange_sig BYTEA,
  IN in_total_amount taler_amount_currency,
  IN in_wire_fee taler_amount_currency,
  IN ina_coin_values taler_amount_currency[],
  IN ina_deposit_fees taler_amount_currency[],
  IN ina_coin_pubs BYTEA[],
  IN ina_contract_terms BYTEA[],
  OUT out_no_instance BOOL,
  OUT out_no_account BOOL,
  OUT out_no_exchange BOOL,
  OUT out_duplicate BOOL,
  OUT out_conflict BOOL)
LANGUAGE plpgsql
AS $$
DECLARE
  my_merchant_id INT8;
  my_signkey_serial INT8;
  my_expected_credit_serial INT8;
  my_affected_orders RECORD;
  my_merchant_serial INT8;
  my_decose INT8;
  my_order_id TEXT;
  i INT8;
  curs CURSOR (arg_coin_pub BYTEA) FOR
    SELECT mcon.deposit_confirmation_serial,
           mcon.order_serial
      FROM merchant_deposits dep
      JOIN merchant_deposit_confirmations mcon
        USING (deposit_confirmation_serial)
      WHERE dep.coin_pub=arg_coin_pub;
  ini_coin_pub BYTEA;
  ini_contract_term BYTEA;
  ini_coin_value taler_amount_currency;
  ini_deposit_fee taler_amount_currency;
BEGIN
SELECT merchant_serial
  INTO my_merchant_id
  FROM merchant_instances
 WHERE merchant_id=in_instance_id;
IF NOT FOUND
THEN
  out_no_instance=TRUE;
  out_no_account=FALSE;
  out_no_exchange=FALSE;
  out_duplicate=FALSE;
  out_conflict=FALSE;
  RETURN;
END IF;
out_no_instance=FALSE;
SELECT expected_credit_serial
  INTO my_expected_credit_serial
  FROM merchant_expected_transfers
 WHERE exchange_url=in_exchange_url
     AND wtid=in_wtid
     AND account_serial=
     (SELECT account_serial
        FROM merchant_accounts
       WHERE payto_uri=in_payto_uri
         AND exchange_url=in_exchange_url
         AND merchant_serial=my_merchant_id);
IF NOT FOUND
THEN
  out_no_account=TRUE;
  out_no_exchange=FALSE;
  out_duplicate=FALSE;
  out_conflict=FALSE;
  RETURN;
END IF;
out_no_account=FALSE;
SELECT signkey_serial
  INTO my_signkey_serial
  FROM merchant_exchange_signing_keys
 WHERE exchange_pub=in_exchange_pub
   ORDER BY start_date DESC
   LIMIT 1;
IF NOT FOUND
THEN
  out_no_exchange=TRUE;
  out_conflict=FALSE;
  out_duplicate=FALSE;
  RETURN;
END IF;
out_no_exchange=FALSE;
INSERT INTO merchant_transfer_signatures
  (expected_credit_serial
  ,signkey_serial
  ,credit_amount
  ,wire_fee
  ,execution_time
  ,exchange_sig)
  VALUES
   (my_expected_credit_serial
   ,my_signkey_serial
   ,in_total_amount
   ,in_wire_fee
   ,in_execution_time
   ,in_exchange_sig)
  ON CONFLICT DO NOTHING;
IF NOT FOUND
THEN
  PERFORM 1
    FROM merchant_transfer_signatures
    WHERE expected_credit_serial=my_expected_credit_serial
      AND signkey_serial=my_signkey_serial
      AND credit_amount=in_total_amount
      AND wire_fee=in_wire_fee
      AND execution_time=in_execution_time
      AND exchange_sig=in_exchange_sig;
  IF FOUND
  THEN
    out_duplicate=TRUE;
    out_conflict=FALSE;
    RETURN;
  END IF;
  out_duplicate=FALSE;
  out_conflict=TRUE;
  RETURN;
END IF;
out_duplicate=FALSE;
out_conflict=FALSE;
FOR i IN 1..array_length(ina_coin_pubs,1)
LOOP
  ini_coin_value=ina_coin_values[i];
  ini_deposit_fee=ina_deposit_fees[i];
  ini_coin_pub=ina_coin_pubs[i];
  ini_contract_term=ina_contract_terms[i];
  INSERT INTO merchant_expected_transfer_to_coin
    (deposit_serial
    ,expected_credit_serial
    ,offset_in_exchange_list
    ,exchange_deposit_value
    ,exchange_deposit_fee)
    SELECT
        dep.deposit_serial
       ,my_expected_credit_serial
       ,i
       ,ini_coin_value
       ,ini_deposit_fee
      FROM merchant_deposits dep
      JOIN merchant_deposit_confirmations dcon
        USING (deposit_confirmation_serial)
      JOIN merchant_contract_terms cterm
        USING (order_serial)
      WHERE dep.coin_pub=ini_coin_pub
        AND cterm.h_contract_terms=ini_contract_term
        AND cterm.merchant_serial=my_merchant_id;
  RAISE NOTICE 'iterating over affected orders';
  OPEN curs (arg_coin_pub:=ini_coin_pub);
  LOOP
    FETCH NEXT FROM curs INTO my_affected_orders;
    EXIT WHEN NOT FOUND;
    RAISE NOTICE 'checking affected order for completion';
    my_decose=my_affected_orders.deposit_confirmation_serial;
    PERFORM FROM merchant_deposits md
       WHERE md.deposit_confirmation_serial=my_decose
         AND settlement_retry_needed
          OR settlement_wtid IS NULL;
    IF NOT FOUND
    THEN
      UPDATE merchant_deposit_confirmations
         SET wire_pending=FALSE
       WHERE (deposit_confirmation_serial=my_decose);
      IF FOUND
      THEN
        RAISE NOTICE 'checking affected contract for completion';
        PERFORM FROM merchant_deposit_confirmations mdc
               WHERE mdc.wire_pending
                 AND mdc.order_serial=my_affected_orders.order_serial;
        IF NOT FOUND
        THEN
          UPDATE merchant_contract_terms
             SET wired=TRUE
           WHERE (order_serial=my_affected_orders.order_serial);
          SELECT merchant_serial, order_id
            INTO my_merchant_serial, my_order_id
            FROM merchant_contract_terms
           WHERE order_serial=my_affected_orders.order_serial;
          INSERT INTO merchant_pending_webhooks
           (merchant_serial
           ,webhook_serial
           ,url
           ,http_method
           ,body)
           SELECT mw.merchant_serial
                 ,mw.webhook_serial
                 ,mw.url
                 ,mw.http_method
                 ,replace_placeholder(
                  replace_placeholder(mw.body_template, 'order_id', my_order_id),
                  'wtid', encode(in_wtid, 'hex')
                  )::TEXT
             FROM merchant_webhook mw
            WHERE mw.event_type = 'order_settled'
              AND mw.merchant_serial = my_merchant_serial;
        END IF; 
      END IF; 
    END IF; 
  END LOOP; 
  CLOSE curs;
END LOOP; 
END $$;
DROP FUNCTION IF EXISTS merchant_do_update_product;
CREATE FUNCTION merchant_do_update_product (
  IN in_instance_id TEXT,
  IN in_product_id TEXT,
  IN in_description TEXT,
  IN in_description_i18n JSONB,
  IN in_unit TEXT,
  IN in_image TEXT,
  IN in_taxes JSONB,
  IN in_price taler_amount_currency,
  IN ina_price_list taler_amount_currency[],
  IN in_total_stock INT8,
  IN in_total_stock_frac INT4,
  IN in_allow_fractional_quantity BOOL,
  IN in_fractional_precision_level INT4,
  IN in_total_lost INT8,
  IN in_address JSONB,
  IN in_next_restock INT8,
  IN in_minimum_age INT4,
  IN ina_categories INT8[],
  IN in_product_name TEXT,
  OUT out_no_instance BOOL,
  OUT out_no_product BOOL,
  OUT out_lost_reduced BOOL,
  OUT out_sold_reduced BOOL,
  OUT out_stocked_reduced BOOL,
  OUT out_no_cat INT8)
LANGUAGE plpgsql
AS $$
DECLARE
  my_merchant_id INT8;
  my_product_serial INT8;
  i INT8;
  ini_cat INT8;
  rec RECORD;
  my_price taler_amount_currency;
  my_price_array taler_amount_currency[];
BEGIN
out_no_instance=FALSE;
out_no_product=FALSE;
out_lost_reduced=FALSE;
out_sold_reduced=FALSE; 
out_stocked_reduced=FALSE;
out_no_cat=NULL;
SELECT merchant_serial
  INTO my_merchant_id
  FROM merchant_instances
 WHERE merchant_id=in_instance_id;
IF NOT FOUND
THEN
  out_no_instance=TRUE;
  RETURN;
END IF;
SELECT total_stock
      ,total_stock_frac
      ,total_lost
      ,allow_fractional_quantity
      ,product_serial
  INTO rec
  FROM merchant_inventory
 WHERE merchant_serial=my_merchant_id
   AND product_id=in_product_id;
IF NOT FOUND
THEN
  out_no_product=TRUE;
  RETURN;
END IF;
my_product_serial = rec.product_serial;
IF COALESCE (array_length(ina_price_list,1),0) = 0
THEN
  my_price_array := ARRAY[in_price]::taler_amount_currency[];
ELSE
  my_price_array := ina_price_list;
END IF;
my_price := my_price_array[1];
IF rec.total_stock > in_total_stock
THEN
  out_stocked_reduced=TRUE;
  RETURN;
END IF;
IF rec.total_lost > in_total_lost
THEN
  out_lost_reduced=TRUE;
  RETURN;
END IF;
IF rec.allow_fractional_quantity
   AND (NOT in_allow_fractional_quantity)
THEN
  DELETE
    FROM merchant_inventory_locks
   WHERE product_serial = my_product_serial
     AND total_locked_frac <> 0;
END IF;
DELETE FROM merchant_product_categories
  WHERE product_serial=my_product_serial;
FOR i IN 1..COALESCE(array_length(ina_categories,1),0)
LOOP
  ini_cat=ina_categories[i];
  INSERT INTO merchant_product_categories
   (product_serial
   ,category_serial)
  VALUES
   (my_product_serial
   ,ini_cat)
  ON CONFLICT DO NOTHING;
  IF NOT FOUND
  THEN
    out_no_cat=i;
    RETURN;
  END IF;
END LOOP;
UPDATE merchant_inventory SET
   description=in_description
  ,description_i18n=in_description_i18n
  ,product_name=in_product_name
  ,unit=in_unit
  ,image=in_image
  ,image_hash=CASE
               WHEN (in_image IS NULL) OR (in_image = '')
               THEN NULL
               ELSE encode(public.digest(convert_to(in_image, 'UTF8'),
                                  'sha256'),
                           'hex')
             END
  ,taxes=in_taxes
  ,price=my_price
  ,price_array=my_price_array
  ,total_stock=in_total_stock
  ,total_stock_frac=in_total_stock_frac
  ,allow_fractional_quantity=in_allow_fractional_quantity
  ,fractional_precision_level=in_fractional_precision_level
  ,total_lost=in_total_lost
  ,address=in_address
  ,next_restock=in_next_restock
  ,minimum_age=in_minimum_age
 WHERE merchant_serial=my_merchant_id
   AND product_serial=my_product_serial; 
ASSERT FOUND,'SELECTED it earlier, should UPDATE it now';
END $$;
DROP FUNCTION IF EXISTS merchant_do_solve_mfa_challenge;
CREATE FUNCTION merchant_do_solve_mfa_challenge (
  IN in_challenge_id INT8,
  IN in_h_body BYTEA,
  IN in_solution TEXT,
  IN in_now INT8,
  OUT out_solved BOOLEAN,
  OUT out_retry_counter INT4
)
LANGUAGE plpgsql
AS $$
DECLARE
  my_confirmation_date INT8;
DECLARE
  my_rec RECORD;
BEGIN
  SELECT
    tc.confirmation_date
   ,tc.retry_counter
   ,(tc.code = in_solution) AS solved
  INTO
    my_rec
  FROM tan_challenges tc
  WHERE tc.challenge_id = in_challenge_id
    AND tc.h_body = in_h_body
    AND tc.expiration_date > in_now;
  IF NOT FOUND
  THEN
    out_solved = FALSE;
    RETURN;
  END IF;
  my_confirmation_date = my_rec.confirmation_date;
  out_retry_counter = my_rec.retry_counter;
  out_solved = my_rec.solved;
  IF my_confirmation_date IS NOT NULL
  THEN
    out_solved = TRUE;
    RETURN;
  END IF;
  IF (0 = out_retry_counter)
  THEN
    out_solved = FALSE;
    RETURN;
  END IF;
  IF out_solved
  THEN
    my_confirmation_date = in_now;
    UPDATE tan_challenges
       SET confirmation_date = my_confirmation_date
     WHERE challenge_id = in_challenge_id;
  ELSE
    out_retry_counter = out_retry_counter - 1;
    UPDATE tan_challenges
       SET retry_counter = out_retry_counter
     WHERE challenge_id = in_challenge_id;
  END IF;
END;
$$;
DROP FUNCTION IF EXISTS merchant_do_account_kyc_set_status;
CREATE FUNCTION merchant_do_account_kyc_set_status (
  IN in_merchant_id TEXT,
  IN in_h_wire BYTEA,
  IN in_exchange_url TEXT,
  IN in_timestamp INT8,
  IN in_exchange_http_status INT4,
  IN in_exchange_ec_code INT4,
  IN in_access_token BYTEA, 
  IN in_jlimits JSONB,
  IN in_aml_active BOOL,
  IN in_kyc_ok BOOL,
  IN in_notify_str TEXT,
  IN in_notify2_str TEXT,
  IN in_rule_gen INT8,
  OUT out_no_instance BOOL,
  OUT out_no_account BOOL)
LANGUAGE plpgsql
AS $$
DECLARE
  my_merchant_id INT8;
  my_account_serial INT8;
BEGIN
out_no_instance=FALSE;
out_no_account=FALSE;
SELECT merchant_serial
  INTO my_merchant_id
  FROM merchant_instances
 WHERE merchant_id=in_merchant_id;
IF NOT FOUND
THEN
  out_no_instance=TRUE;
  RETURN;
END IF;
SELECT account_serial
  INTO my_account_serial
  FROM merchant_accounts
 WHERE merchant_serial=my_merchant_id
   AND h_wire=in_h_wire;
IF NOT FOUND
THEN
  out_no_account=TRUE;
  RETURN;
END IF;
UPDATE merchant_kyc
   SET kyc_timestamp=in_timestamp
      ,kyc_ok=in_kyc_ok
      ,jaccount_limits=in_jlimits
      ,aml_review=in_aml_active
      ,exchange_http_status=in_exchange_http_status
      ,exchange_ec_code=in_exchange_ec_code
      ,access_token=in_access_token
      ,last_rule_gen=in_rule_gen
 WHERE account_serial=my_account_serial
   AND exchange_url=in_exchange_url;
IF NOT FOUND
THEN
  INSERT INTO merchant_kyc
    (kyc_timestamp
    ,kyc_ok
    ,account_serial
    ,exchange_url
    ,jaccount_limits
    ,aml_review
    ,exchange_http_status
    ,exchange_ec_code
    ,access_token
    ,last_rule_gen)
  VALUES
    (in_timestamp
    ,in_kyc_ok
    ,my_account_serial
    ,in_exchange_url
    ,in_jlimits
    ,in_aml_active
    ,in_exchange_http_status
    ,in_exchange_ec_code
    ,in_access_token
    ,in_rule_gen);
END IF;
EXECUTE FORMAT (
   'NOTIFY %s'
  ,in_notify_str);
EXECUTE FORMAT (
   'NOTIFY %s'
  ,in_notify2_str);
END $$;
DROP FUNCTION IF EXISTS merchant_do_account_kyc_set_failed;
CREATE FUNCTION merchant_do_account_kyc_set_failed (
  IN in_merchant_id TEXT,
  IN in_h_wire BYTEA,
  IN in_exchange_url TEXT,
  IN in_timestamp INT8,
  IN in_exchange_http_status INT4,
  IN in_kyc_ok BOOL,
  IN in_notify_str TEXT,
  IN in_notify2_str TEXT,
  OUT out_no_instance BOOL,
  OUT out_no_account BOOL)
LANGUAGE plpgsql
AS $$
DECLARE
  my_merchant_id INT8;
  my_account_serial INT8;
BEGIN
out_no_instance=FALSE;
out_no_account=FALSE;
SELECT merchant_serial
  INTO my_merchant_id
  FROM merchant_instances
 WHERE merchant_id=in_merchant_id;
IF NOT FOUND
THEN
  out_no_instance=TRUE;
  RETURN;
END IF;
SELECT account_serial
  INTO my_account_serial
  FROM merchant_accounts
 WHERE merchant_serial=my_merchant_id
   AND h_wire=in_h_wire;
IF NOT FOUND
THEN
  out_no_account=TRUE;
  RETURN;
END IF;
UPDATE merchant_kyc
   SET kyc_timestamp=in_timestamp
      ,kyc_ok=in_kyc_ok
      ,exchange_http_status=in_exchange_http_status
      ,exchange_ec_code=0
 WHERE account_serial=my_account_serial
   AND exchange_url=in_exchange_url;
IF NOT FOUND
THEN
  INSERT INTO merchant_kyc
    (kyc_timestamp
    ,kyc_ok
    ,account_serial
    ,exchange_url
    ,exchange_http_status)
  VALUES
    (in_timestamp
    ,in_kyc_ok
    ,my_account_serial
    ,in_exchange_url
    ,in_exchange_http_status);
END IF;
EXECUTE FORMAT (
   'NOTIFY %s'
  ,in_notify_str);
EXECUTE FORMAT (
   'NOTIFY %s'
  ,in_notify2_str);
END $$;
SET search_path TO merchant;
DROP FUNCTION IF EXISTS interval_to_start;
CREATE OR REPLACE FUNCTION interval_to_start (
  IN in_timestamp TIMESTAMP,
  IN in_range statistic_range,
  OUT out_bucket_start INT8
)
LANGUAGE plpgsql
AS $$
BEGIN
  out_bucket_start = EXTRACT(EPOCH FROM DATE_TRUNC(in_range::text, in_timestamp));
END $$;
COMMENT ON FUNCTION interval_to_start
 IS 'computes the start time of the bucket for an event at the current time given the desired bucket range';
DROP PROCEDURE IF EXISTS merchant_do_bump_number_bucket_stat;
CREATE OR REPLACE PROCEDURE merchant_do_bump_number_bucket_stat(
  in_slug TEXT,
  in_merchant_serial BIGINT,
  in_timestamp TIMESTAMP,
  in_delta INT8
)
LANGUAGE plpgsql
AS $$
DECLARE
  my_meta INT8;
  my_range statistic_range;
  my_bucket_start INT8;
  my_curs CURSOR (arg_slug TEXT)
   FOR SELECT UNNEST(ranges)
         FROM merchant_statistic_bucket_meta
        WHERE slug=arg_slug;
BEGIN
  SELECT bmeta_serial_id
    INTO my_meta
    FROM merchant_statistic_bucket_meta
   WHERE slug=in_slug
     AND stype='number';
  IF NOT FOUND
  THEN
    RETURN;
  END IF;
  OPEN my_curs (arg_slug:=in_slug);
  LOOP
    FETCH NEXT
      FROM my_curs
      INTO my_range;
    EXIT WHEN NOT FOUND;
    SELECT *
      INTO my_bucket_start
      FROM interval_to_start (in_timestamp, my_range);
    UPDATE merchant_statistic_bucket_counter
       SET cumulative_number = cumulative_number + in_delta
     WHERE bmeta_serial_id=my_meta
       AND merchant_serial=in_merchant_serial
       AND bucket_start=my_bucket_start
       AND bucket_range=my_range;
    IF NOT FOUND
    THEN
      INSERT INTO merchant_statistic_bucket_counter
        (bmeta_serial_id
        ,merchant_serial
        ,bucket_start
        ,bucket_range
        ,cumulative_number
        ) VALUES (
         my_meta
        ,in_merchant_serial
        ,my_bucket_start
        ,my_range
        ,in_delta);
    END IF;
  END LOOP;
  CLOSE my_curs;
END $$;
DROP PROCEDURE IF EXISTS merchant_do_bump_amount_bucket_stat;
CREATE OR REPLACE PROCEDURE merchant_do_bump_amount_bucket_stat(
  in_slug TEXT,
  in_merchant_serial BIGINT,
  in_timestamp TIMESTAMP,
  in_delta taler_amount_currency
)
LANGUAGE plpgsql
AS $$
DECLARE
  my_meta INT8;
  my_range statistic_range;
  my_bucket_start INT8;
  my_curs CURSOR (arg_slug TEXT)
   FOR SELECT UNNEST(ranges)
         FROM merchant_statistic_bucket_meta
        WHERE slug=arg_slug;
BEGIN
  SELECT bmeta_serial_id
    INTO my_meta
    FROM merchant_statistic_bucket_meta
   WHERE slug=in_slug
     AND stype='amount';
  IF NOT FOUND
  THEN
    RETURN;
  END IF;
  OPEN my_curs (arg_slug:=in_slug);
  LOOP
    FETCH NEXT
      FROM my_curs
      INTO my_range;
    EXIT WHEN NOT FOUND;
    SELECT *
      INTO my_bucket_start
      FROM interval_to_start (in_timestamp, my_range);
    UPDATE merchant_statistic_bucket_amount
      SET
        cumulative_value = cumulative_value + (in_delta).val
        + CASE
            WHEN (in_delta).frac + cumulative_frac >= 100000000
            THEN 1
            ELSE 0
          END,
        cumulative_frac = cumulative_frac + (in_delta).frac
        - CASE
            WHEN (in_delta).frac + cumulative_frac >= 100000000
            THEN 100000000
            ELSE 0
          END
     WHERE bmeta_serial_id=my_meta
       AND merchant_serial=in_merchant_serial
       AND curr=(in_delta).curr
       AND bucket_start=my_bucket_start
       AND bucket_range=my_range;
    IF NOT FOUND
    THEN
      INSERT INTO merchant_statistic_bucket_amount
        (bmeta_serial_id
        ,merchant_serial
        ,bucket_start
        ,bucket_range
        ,curr
        ,cumulative_value
        ,cumulative_frac
        ) VALUES (
         my_meta
        ,in_merchant_serial
        ,my_bucket_start
        ,my_range
        ,(in_delta).curr
        ,(in_delta).val
        ,(in_delta).frac);
    END IF;
  END LOOP;
  CLOSE my_curs;
END $$;
COMMENT ON PROCEDURE merchant_do_bump_amount_bucket_stat
  IS 'Updates an amount statistic tracked over buckets';
DROP PROCEDURE IF EXISTS merchant_do_bump_number_interval_stat;
CREATE OR REPLACE PROCEDURE merchant_do_bump_number_interval_stat(
  in_slug TEXT,
  in_merchant_serial BIGINT,
  in_timestamp TIMESTAMP,
  in_delta INT8
)
LANGUAGE plpgsql
AS $$
DECLARE
  my_now INT8;
  my_record RECORD;
  my_meta INT8;
  my_ranges INT8[];
  my_precisions INT8[];
  my_rangex INT8;
  my_precisionx INT8;
  my_start INT8;
  my_event INT8;
BEGIN
  my_now = ROUND(EXTRACT(epoch FROM CURRENT_TIMESTAMP(0)::TIMESTAMP) * 1000000)::INT8 / 1000 / 1000;
  SELECT imeta_serial_id
        ,ranges AS ranges
        ,precisions AS precisions
    INTO my_record
    FROM merchant_statistic_interval_meta
   WHERE slug=in_slug
     AND stype='number';
  IF NOT FOUND
  THEN
    RETURN;
  END IF;
  my_start = ROUND(EXTRACT(epoch FROM in_timestamp) * 1000000)::INT8 / 1000 / 1000; 
  my_precisions = my_record.precisions;
  my_ranges = my_record.ranges;
  my_rangex = NULL;
  FOR my_x IN 1..COALESCE(array_length(my_ranges,1),0)
  LOOP
    IF my_now - my_ranges[my_x] < my_start
    THEN
      my_rangex = my_ranges[my_x];
      my_precisionx = my_precisions[my_x];
      EXIT;
    END IF;
  END LOOP;
  IF my_rangex IS NULL
  THEN
    RETURN;
  END IF;
  my_meta = my_record.imeta_serial_id;
  my_start = my_start - my_start % my_precisionx; 
  INSERT INTO merchant_statistic_counter_event AS msce
    (imeta_serial_id
    ,merchant_serial
    ,slot
    ,delta)
   VALUES
    (my_meta
    ,in_merchant_serial
    ,my_start
    ,in_delta)
   ON CONFLICT (imeta_serial_id, merchant_serial, slot)
   DO UPDATE SET
     delta = msce.delta + in_delta
   RETURNING nevent_serial_id
        INTO my_event;
  UPDATE merchant_statistic_interval_counter
     SET cumulative_number = cumulative_number + in_delta
   WHERE imeta_serial_id = my_meta
     AND merchant_serial = in_merchant_serial
     AND range=my_rangex;
  IF NOT FOUND
  THEN
    INSERT INTO merchant_statistic_interval_counter
      (imeta_serial_id
      ,merchant_serial
      ,range
      ,event_delimiter
      ,cumulative_number
     ) VALUES (
       my_meta
      ,in_merchant_serial
      ,my_rangex
      ,my_event
      ,in_delta);
  END IF;
END $$;
COMMENT ON PROCEDURE merchant_do_bump_number_interval_stat
  IS 'Updates a numeric statistic tracked over an interval';
DROP PROCEDURE IF EXISTS merchant_do_bump_amount_interval_stat;
CREATE OR REPLACE PROCEDURE merchant_do_bump_amount_interval_stat(
  in_slug TEXT,
  in_merchant_serial BIGINT,
  in_timestamp TIMESTAMP,
  in_delta taler_amount_currency 
)
LANGUAGE plpgsql
AS $$
DECLARE
  my_now INT8;
  my_record RECORD;
  my_meta INT8;
  my_ranges INT8[];
  my_precisions INT8[];
  my_x INT;
  my_rangex INT8;
  my_precisionx INT8;
  my_start INT8;
  my_event INT8;
BEGIN
  my_now = ROUND(EXTRACT(epoch FROM CURRENT_TIMESTAMP(0)::TIMESTAMP) * 1000000)::INT8 / 1000 / 1000;
  SELECT imeta_serial_id
        ,ranges
        ,precisions
    INTO my_record
    FROM merchant_statistic_interval_meta
   WHERE slug=in_slug
     AND stype='amount';
  IF NOT FOUND
  THEN
    RETURN;
  END IF;
  my_start = ROUND(EXTRACT(epoch FROM in_timestamp) * 1000000)::INT8 / 1000 / 1000; 
  my_precisions = my_record.precisions;
  my_ranges = my_record.ranges;
  my_rangex = NULL;
  FOR my_x IN 1..COALESCE(array_length(my_ranges,1),0)
  LOOP
    IF my_now - my_ranges[my_x] < my_start
    THEN
      my_rangex = my_ranges[my_x];
      my_precisionx = my_precisions[my_x];
      EXIT;
    END IF;
  END LOOP;
  IF my_rangex IS NULL
  THEN
    RETURN;
  END IF;
  my_start = my_start - my_start % my_precisionx; 
  my_meta = my_record.imeta_serial_id;
  INSERT INTO merchant_statistic_amount_event AS msae
    (imeta_serial_id
    ,merchant_serial
    ,slot
    ,delta_curr
    ,delta_value
    ,delta_frac
    ) VALUES (
     my_meta
    ,in_merchant_serial
    ,my_start
    ,(in_delta).curr
    ,(in_delta).val
    ,(in_delta).frac
    )
    ON CONFLICT (imeta_serial_id, merchant_serial, slot, delta_curr)
    DO UPDATE SET
      delta_value = msae.delta_value + (in_delta).val
        + CASE
          WHEN (in_delta).frac + msae.delta_frac >= 100000000
          THEN 1
          ELSE 0
        END,
      delta_frac = msae.delta_frac + (in_delta).frac
        - CASE
          WHEN (in_delta).frac + msae.delta_frac >= 100000000
          THEN 100000000
          ELSE 0
        END
    RETURNING aevent_serial_id
         INTO my_event;
  UPDATE merchant_statistic_interval_amount
    SET
      cumulative_value = cumulative_value + (in_delta).val
      + CASE
          WHEN (in_delta).frac + cumulative_frac >= 100000000
          THEN 1
          ELSE 0
        END,
      cumulative_frac = cumulative_frac + (in_delta).frac
      - CASE
          WHEN (in_delta).frac + cumulative_frac >= 100000000
          THEN 100000000
          ELSE 0
        END
   WHERE imeta_serial_id=my_meta
     AND merchant_serial=in_merchant_serial
     AND range=my_rangex
     AND curr=(in_delta).curr;
  IF NOT FOUND
  THEN
    INSERT INTO merchant_statistic_interval_amount
      (imeta_serial_id
      ,merchant_serial
      ,range
      ,event_delimiter
      ,curr
      ,cumulative_value
      ,cumulative_frac
      ) VALUES (
       my_meta
      ,in_merchant_serial
      ,my_rangex
      ,my_event
      ,(in_delta).curr
      ,(in_delta).val
      ,(in_delta).frac);
  END IF;
END $$;
COMMENT ON PROCEDURE merchant_do_bump_amount_interval_stat
  IS 'Updates an amount statistic tracked over an interval';
DROP PROCEDURE IF EXISTS merchant_do_bump_number_stat;
CREATE OR REPLACE PROCEDURE merchant_do_bump_number_stat(
  in_slug TEXT,
  in_merchant_serial BIGINT,
  in_timestamp TIMESTAMP,
  in_delta INT8
)
LANGUAGE plpgsql
AS $$
BEGIN
  CALL merchant_do_bump_number_bucket_stat (in_slug, in_merchant_serial, in_timestamp, in_delta);
  CALL merchant_do_bump_number_interval_stat (in_slug, in_merchant_serial, in_timestamp, in_delta);
END $$;
COMMENT ON PROCEDURE merchant_do_bump_number_stat
  IS 'Updates a numeric statistic (bucket or interval)';
DROP PROCEDURE IF EXISTS merchant_do_bump_amount_stat;
CREATE OR REPLACE PROCEDURE merchant_do_bump_amount_stat(
  in_slug TEXT,
  in_merchant_serial BIGINT,
  in_timestamp TIMESTAMP,
  in_delta taler_amount_currency
)
LANGUAGE plpgsql
AS $$
BEGIN
  CALL merchant_do_bump_amount_bucket_stat (in_slug, in_merchant_serial, in_timestamp, in_delta);
  CALL merchant_do_bump_amount_interval_stat (in_slug, in_merchant_serial, in_timestamp, in_delta);
END $$;
COMMENT ON PROCEDURE merchant_do_bump_amount_stat
  IS 'Updates an amount statistic (bucket or interval)';
DROP FUNCTION IF EXISTS merchant_statistic_interval_number_get;
CREATE FUNCTION merchant_statistic_interval_number_get (
  IN in_slug TEXT,
  IN in_instance_id TEXT
)
RETURNS SETOF merchant_statistic_interval_number_get_return_value
LANGUAGE plpgsql
AS $$
DECLARE
  my_time INT8 DEFAULT ROUND(EXTRACT(epoch FROM CURRENT_TIMESTAMP(0)::TIMESTAMP) * 1000000)::INT8 / 1000 / 1000;
  my_ranges INT8[];
  my_range INT8;
  my_delta INT8;
  my_meta INT8;
  my_next_max_serial INT8;
  my_instance_id INT8;
  my_rec RECORD;
  my_irec RECORD;
  my_i INT;
  my_min_serial INT8 DEFAULT NULL;
  my_rval merchant_statistic_interval_number_get_return_value;
BEGIN
  SELECT merchant_serial
    INTO my_instance_id
    FROM merchant_instances
   WHERE merchant_id=in_instance_id;
  IF NOT FOUND
  THEN
    RETURN;
  END IF;
  SELECT imeta_serial_id
        ,ranges
        ,precisions
    INTO my_rec
    FROM merchant_statistic_interval_meta
   WHERE slug=in_slug;
  IF NOT FOUND
  THEN
    RETURN;
  END IF;
  my_rval.rvalue = 0;
  my_ranges = my_rec.ranges;
  my_meta = my_rec.imeta_serial_id;
  FOR my_i IN 1..COALESCE(array_length(my_ranges,1),0)
  LOOP
    my_range = my_ranges[my_i];
    SELECT event_delimiter
          ,cumulative_number
      INTO my_irec
      FROM merchant_statistic_interval_counter
     WHERE imeta_serial_id = my_meta
       AND range = my_range
       AND merchant_serial = my_instance_id;
    IF FOUND
    THEN
      my_min_serial = my_irec.event_delimiter;
      my_rval.rvalue = my_rval.rvalue + my_irec.cumulative_number;
      SELECT SUM(delta) AS delta_sum
        INTO my_irec
        FROM merchant_statistic_counter_event
       WHERE imeta_serial_id = my_meta
         AND merchant_serial = my_instance_id
         AND slot < my_time - my_range
         AND nevent_serial_id >= my_min_serial;
      IF FOUND AND my_irec.delta_sum IS NOT NULL
      THEN
        my_delta = my_irec.delta_sum;
        my_rval.rvalue = my_rval.rvalue - my_delta;
        SELECT nevent_serial_id
          INTO my_next_max_serial
          FROM merchant_statistic_counter_event
         WHERE imeta_serial_id = my_meta
           AND merchant_serial = my_instance_id
           AND slot >= my_time - my_range
           AND nevent_serial_id >= my_min_serial
         ORDER BY slot ASC
         LIMIT 1;
        IF FOUND
        THEN
          UPDATE merchant_statistic_interval_counter
             SET cumulative_number = cumulative_number - my_delta,
                 event_delimiter = my_next_max_serial
           WHERE imeta_serial_id = my_meta
             AND merchant_serial = my_instance_id
             AND range = my_range;
        ELSE
          DELETE FROM merchant_statistic_interval_counter
           WHERE imeta_serial_id = my_meta
             AND merchant_serial = my_instance_id
             AND range = my_range;
        END IF;
        IF (my_i < array_length(my_ranges,1))
        THEN
          UPDATE merchant_statistic_interval_counter AS usic SET
            cumulative_number = cumulative_number + my_delta,
            event_delimiter = LEAST(usic.event_delimiter,my_min_serial)
           WHERE imeta_serial_id = my_meta
             AND merchant_serial = my_instance_id
             AND range=my_ranges[my_i+1];
          IF NOT FOUND
          THEN
            INSERT INTO merchant_statistic_interval_counter
              (imeta_serial_id
              ,merchant_serial
              ,range
              ,event_delimiter
              ,cumulative_number
              ) VALUES (
               my_meta
              ,my_instance_id
              ,my_ranges[my_i+1]
              ,my_min_serial
              ,my_delta);
          END IF;
        ELSE
          DELETE FROM merchant_statistic_counter_event
                WHERE imeta_serial_id = my_meta
                  AND merchant_serial = my_instance_id
                  AND slot < my_time - my_range;
        END IF;
      END IF;
      my_rval.range = my_range;
      RETURN NEXT my_rval;
    END IF;
  END LOOP;
END $$;
COMMENT ON FUNCTION merchant_statistic_interval_number_get
  IS 'Returns deposit statistic tracking deposited amounts over certain time intervals; we first trim the stored data to only track what is still in-range, and then return the remaining value for each range';
DROP FUNCTION IF EXISTS merchant_statistic_interval_amount_get;
CREATE FUNCTION merchant_statistic_interval_amount_get (
  IN in_slug TEXT,
  IN in_instance_id TEXT
)
RETURNS SETOF merchant_statistic_interval_amount_get_return_value
LANGUAGE plpgsql
AS $$
DECLARE
  my_time INT8 DEFAULT ROUND(EXTRACT(epoch FROM CURRENT_TIMESTAMP(0)::TIMESTAMP) * 1000000)::INT8 / 1000 / 1000;
  my_ranges INT8[];
  my_range INT8;
  my_delta_value INT8;
  my_delta_frac INT8;
  my_meta INT8;
  my_instance_id INT8;
  my_next_max_serial INT8;
  my_currency TEXT;
  my_rec RECORD;
  my_irec RECORD;
  my_jrec RECORD;
  my_i INT;
  my_min_serial INT8 DEFAULT NULL;
  my_rval merchant_statistic_interval_amount_get_return_value;
BEGIN
  SELECT merchant_serial
    INTO my_instance_id
    FROM merchant_instances
   WHERE merchant_id=in_instance_id;
  IF NOT FOUND
  THEN
    RETURN;
  END IF;
  SELECT imeta_serial_id
        ,ranges
        ,precisions
    INTO my_rec
    FROM merchant_statistic_interval_meta
   WHERE slug=in_slug;
  IF NOT FOUND
  THEN
    RETURN;
  END IF;
  my_meta = my_rec.imeta_serial_id;
  my_ranges = my_rec.ranges;
  FOR my_currency IN
    SELECT DISTINCT delta_curr
      FROM merchant_statistic_amount_event
     WHERE imeta_serial_id = my_meta
  LOOP
  my_rval.rvalue.val = 0;
  my_rval.rvalue.frac = 0;
  my_rval.rvalue.curr = my_currency;
  FOR my_i IN 1..COALESCE(array_length(my_ranges,1),0)
  LOOP
    my_range = my_ranges[my_i];
    SELECT event_delimiter
          ,cumulative_value
          ,cumulative_frac
      INTO my_irec
      FROM merchant_statistic_interval_amount
     WHERE imeta_serial_id = my_meta
       AND merchant_serial = my_instance_id
       AND curr = my_currency
       AND range = my_range;
    IF FOUND
    THEN
      my_min_serial = my_irec.event_delimiter;
      my_rval.rvalue.val = (my_rval.rvalue).val + my_irec.cumulative_value + my_irec.cumulative_frac / 100000000;
      my_rval.rvalue.frac = (my_rval.rvalue).frac + my_irec.cumulative_frac % 100000000;
      IF (my_rval.rvalue).frac > 100000000
      THEN
        my_rval.rvalue.frac = (my_rval.rvalue).frac - 100000000;
        my_rval.rvalue.val = (my_rval.rvalue).val + 1;
      END IF;
      SELECT SUM(delta_value) AS value_sum
            ,SUM(delta_frac) AS frac_sum
        INTO my_jrec
        FROM merchant_statistic_amount_event
       WHERE imeta_serial_id = my_meta
         AND merchant_serial = my_instance_id
         AND delta_curr = my_currency
         AND slot < my_time - my_range
         AND aevent_serial_id >= my_min_serial;
      IF FOUND AND my_jrec.value_sum IS NOT NULL
      THEN
        my_delta_value = my_jrec.value_sum + my_jrec.frac_sum / 100000000;
        my_delta_frac = my_jrec.frac_sum % 100000000;
        my_rval.rvalue.val = (my_rval.rvalue).val - my_delta_value;
        IF ((my_rval.rvalue).frac >= my_delta_frac)
        THEN
          my_rval.rvalue.frac = (my_rval.rvalue).frac - my_delta_frac;
        ELSE
          my_rval.rvalue.frac = 100000000 + (my_rval.rvalue).frac - my_delta_frac;
          my_rval.rvalue.val = (my_rval.rvalue).val - 1;
        END IF;
        SELECT aevent_serial_id
          INTO my_next_max_serial
          FROM merchant_statistic_amount_event
         WHERE imeta_serial_id = my_meta
           AND merchant_serial = my_instance_id
           AND delta_curr = my_currency
           AND slot >= my_time - my_range
           AND aevent_serial_id >= my_min_serial
         ORDER BY slot ASC
         LIMIT 1;
        IF FOUND
        THEN
          UPDATE merchant_statistic_interval_amount SET
             cumulative_value = cumulative_value - my_delta_value
              - CASE
                  WHEN cumulative_frac < my_delta_frac
                  THEN 1
                  ELSE 0
                END,
             cumulative_frac = cumulative_frac - my_delta_frac
             + CASE
                 WHEN cumulative_frac < my_delta_frac
                 THEN 100000000
                 ELSE 0
               END,
             event_delimiter = my_next_max_serial
           WHERE imeta_serial_id = my_meta
             AND merchant_serial = my_instance_id
             AND curr = my_currency
             AND range = my_range;
        ELSE
          DELETE FROM merchant_statistic_interval_amount
           WHERE imeta_serial_id = my_meta
             AND merchant_serial = my_instance_id
             AND curr = my_currency
             AND range = my_range;
        END IF;
        IF (my_i < array_length(my_ranges,1))
        THEN
          UPDATE merchant_statistic_interval_amount AS msia SET
            cumulative_value = cumulative_value + my_delta_value
              + CASE
                 WHEN cumulative_frac + my_delta_frac > 100000000
                 THEN 1
                 ELSE 0
               END,
            cumulative_frac = cumulative_frac + my_delta_value
              - CASE
                 WHEN cumulative_frac + my_delta_frac > 100000000
                 THEN 100000000
                 ELSE 0
               END,
            event_delimiter = LEAST (msia.event_delimiter,my_min_serial)
           WHERE imeta_serial_id = my_meta
             AND merchant_serial = my_instance_id
             AND range=my_ranges[my_i+1];
          IF NOT FOUND
          THEN
            INSERT INTO merchant_statistic_interval_amount
              (imeta_serial_id
              ,merchant_serial
              ,event_delimiter
              ,range
              ,curr
              ,cumulative_value
              ,cumulative_frac
              ) VALUES (
               my_meta
              ,my_instance_id
              ,my_min_serial
              ,my_ranges[my_i+1]
              ,my_currency
              ,my_delta_value
              ,my_delta_frac);
          END IF;
        ELSE
          DELETE FROM merchant_statistic_amount_event
                WHERE imeta_serial_id = my_meta
                  AND merchant_serial = my_instance_id
                  AND slot < my_time - my_range;
        END IF;
      END IF;
      my_rval.range = my_range;
      RETURN NEXT my_rval;
    END IF;
  END LOOP; 
  END LOOP; 
END $$;
COMMENT ON FUNCTION merchant_statistic_interval_amount_get
  IS 'Returns deposit statistic tracking deposited amounts over certain time intervals; we first trim the stored data to only track what is still in-range, and then return the remaining value; multiple values are returned, one per currency and range';
DROP PROCEDURE IF EXISTS merchant_statistic_counter_gc;
CREATE OR REPLACE PROCEDURE merchant_statistic_counter_gc ()
LANGUAGE plpgsql
AS $$
DECLARE
  my_time INT8 DEFAULT ROUND(EXTRACT(epoch FROM CURRENT_TIMESTAMP(0)::TIMESTAMP) * 1000000)::INT8 / 1000 / 1000;
  my_instance INT8;
  my_instance_name TEXT;
  my_rec RECORD;
  my_sum RECORD;
  my_meta INT8;
  my_ranges INT8[];
  my_precisions INT8[];
  my_precision INT4;
  my_i INT4;
  min_slot INT8;
  max_slot INT8;
  end_slot INT8;
  my_total INT8;
BEGIN
  FOR my_instance IN
    SELECT DISTINCT merchant_serial
      FROM merchant_statistic_counter_event
  LOOP
  FOR my_rec IN
    SELECT imeta_serial_id
          ,ranges
          ,precisions
          ,slug
      FROM merchant_statistic_interval_meta
  LOOP
    SELECT merchant_id
      INTO my_instance_name
       FROM merchant_instances
      WHERE merchant_serial = my_instance;
    PERFORM FROM merchant_statistic_interval_number_get (my_rec.slug, my_instance_name);
    my_meta = my_rec.imeta_serial_id;
    my_ranges = my_rec.ranges;
    my_precisions = my_rec.precisions;
    FOR my_i IN 1..COALESCE(array_length(my_ranges,1),0)
    LOOP
      my_precision = my_precisions[my_i];
      IF 1 >= my_precision
      THEN
        CONTINUE;
      END IF;
      IF 1 = my_i
      THEN
        min_slot = 0;
      ELSE
        min_slot = my_ranges[my_i - 1];
      END IF;
      end_slot = my_ranges[my_i];
      RAISE NOTICE 'Coarsening from [%,%) at %', my_time - end_slot, my_time - min_slot, my_precision;
      LOOP
        EXIT WHEN min_slot >= end_slot;
        max_slot = min_slot + my_precision;
        SELECT SUM(delta) AS total,
               COUNT(*) AS matches,
               MIN(nevent_serial_id) AS rep_serial_id
          INTO my_sum
          FROM merchant_statistic_counter_event
         WHERE merchant_serial=my_instance
           AND imeta_serial_id=my_meta
           AND slot >= my_time - max_slot
           AND slot < my_time - min_slot;
        RAISE NOTICE 'Found % entries between [%,%)', my_sum.matches, my_time - max_slot, my_time - min_slot;
        IF FOUND AND my_sum.matches > 1
        THEN
          my_total = my_sum.total;
          RAISE NOTICE 'combining % entries to representative % for slots [%-%)', my_sum.matches, my_sum.rep_serial_id, my_time - max_slot, my_time - min_slot;
          DELETE FROM merchant_statistic_counter_event
           WHERE merchant_serial=my_instance
             AND imeta_serial_id=my_meta
             AND slot >= my_time - max_slot
             AND slot < my_time - min_slot
             AND nevent_serial_id > my_sum.rep_serial_id;
          UPDATE merchant_statistic_counter_event SET
            delta = my_total
           WHERE imeta_serial_id = my_meta
             AND merchant_serial = my_instance
             AND nevent_serial_id = my_sum.rep_serial_id;
        END IF;
        min_slot = min_slot + my_precision;
      END LOOP; 
    END LOOP; 
    RAISE NOTICE 'deleting entries of %/% before % - % = %', my_instance, my_meta, my_time, my_ranges[array_length(my_ranges,1)], my_time - my_ranges[array_length(my_ranges,1)];
    DELETE FROM merchant_statistic_counter_event
     WHERE merchant_serial=my_instance
       AND imeta_serial_id=my_meta
       AND slot < my_time - my_ranges[array_length(my_ranges,1)];
  END LOOP; 
  END LOOP; 
END $$;
COMMENT ON PROCEDURE merchant_statistic_counter_gc
  IS 'Performs garbage collection and compaction of the merchant_statistic_counter_event table';
DROP PROCEDURE IF EXISTS merchant_statistic_amount_gc;
CREATE OR REPLACE PROCEDURE merchant_statistic_amount_gc ()
LANGUAGE plpgsql
AS $$
DECLARE
  my_time INT8 DEFAULT ROUND(EXTRACT(epoch FROM CURRENT_TIMESTAMP(0)::TIMESTAMP) * 1000000)::INT8 / 1000 / 1000;
  my_instance INT8;
  my_instance_name TEXT;
  my_rec RECORD;
  my_sum RECORD;
  my_meta INT8;
  my_ranges INT8[];
  my_precisions INT8[];
  my_precision INT4;
  my_currency TEXT;
  my_i INT4;
  min_slot INT8;
  max_slot INT8;
  end_slot INT8;
  my_total_val INT8;
  my_total_frac INT8;
BEGIN
  FOR my_instance IN
    SELECT DISTINCT merchant_serial
      FROM merchant_statistic_counter_event
  LOOP
  FOR my_rec IN
    SELECT imeta_serial_id
          ,ranges
          ,precisions
          ,slug
      FROM merchant_statistic_interval_meta
  LOOP
  SELECT merchant_id
    INTO my_instance_name
     FROM merchant_instances
    WHERE merchant_serial = my_instance;
  PERFORM FROM merchant_statistic_interval_amount_get (my_rec.slug, my_instance_name);
  my_meta = my_rec.imeta_serial_id;
  my_ranges = my_rec.ranges;
  my_precisions = my_rec.precisions;
  FOR my_currency IN
    SELECT DISTINCT delta_curr
      FROM merchant_statistic_amount_event
     WHERE imeta_serial_id = my_meta
  LOOP
    FOR my_i IN 1..COALESCE(array_length(my_ranges,1),0)
    LOOP
      my_precision = my_precisions[my_i];
      IF 1 >= my_precision
      THEN
        CONTINUE;
      END IF;
      IF 1 = my_i
      THEN
        min_slot = 0;
      ELSE
        min_slot = my_ranges[my_i - 1];
      END IF;
      end_slot = my_ranges[my_i];
      RAISE NOTICE 'Coarsening from [%,%) at %', my_time - end_slot, my_time - min_slot, my_precision;
      LOOP
        EXIT WHEN min_slot >= end_slot;
        max_slot = min_slot + my_precision;
        SELECT SUM(delta_value) AS total_val,
               SUM(delta_frac) AS total_frac,
               COUNT(*) AS matches,
               MIN(aevent_serial_id) AS rep_serial_id
          INTO my_sum
          FROM merchant_statistic_amount_event
         WHERE imeta_serial_id=my_meta
           AND merchant_serial=my_instance
           AND delta_curr = my_currency
           AND slot >= my_time - max_slot
           AND slot < my_time - max_slot;
        IF FOUND AND my_sum.matches > 1
        THEN
          my_total_frac = my_sum.total_frac % 100000000;
          my_total_val = my_sum.total_val + my_sum.total_frac / 100000000;
          DELETE FROM merchant_statistic_amount_event
           WHERE imeta_serial_id=my_meta
             AND merchant_serial=my_instance
             AND delta_curr = my_currency
             AND slot >= my_time - max_slot
             AND slot < my_time - max_slot
             AND aevent_serial_id > my_sum.rep_serial_id;
          UPDATE merchant_statistic_amount_event SET
             delta_value = my_total_value
            ,delta_frac = my_total_frac
           WHERE imeta_serial_id = my_meta
             AND merchant_serial = my_instance
             AND delta_curr = my_currency
             AND aevent_serial_id = my_sum.rep_serial_id;
        END IF;
        min_slot = min_slot + my_precision;
      END LOOP; 
    END LOOP; 
  END LOOP; 
  RAISE NOTICE 'deleting entries of %/% before % - % = %', my_instance, my_meta, my_time, my_ranges[array_length(my_ranges,1)], my_time - my_ranges[array_length(my_ranges,1)];
  DELETE FROM merchant_statistic_amount_event
   WHERE merchant_serial=my_instance
     AND imeta_serial_id=my_meta
     AND slot < my_time - my_ranges[array_length(my_ranges,1)];
  END LOOP; 
  END LOOP; 
END $$;
COMMENT ON PROCEDURE merchant_statistic_amount_gc
  IS 'Performs garbage collection and compaction of the merchant_statistic_amount_event table';
DROP PROCEDURE IF EXISTS merchant_statistic_bucket_gc;
CREATE OR REPLACE PROCEDURE merchant_statistic_bucket_gc ()
LANGUAGE plpgsql
AS $$
DECLARE
  my_rec RECORD;
  my_range TEXT;
  my_now INT8;
  my_end INT8;
BEGIN
  my_now = EXTRACT(EPOCH FROM CURRENT_TIMESTAMP(0)::TIMESTAMP); 
  FOR my_rec IN
    SELECT bmeta_serial_id
          ,stype
          ,ranges[array_length(ranges,1)] AS range
          ,ages[array_length(ages,1)] AS age
      FROM merchant_statistic_bucket_meta
  LOOP
    my_range = '1 ' || my_rec.range::TEXT;
    my_end = my_now - my_rec.age * EXTRACT(SECONDS FROM (SELECT my_range::INTERVAL)); 
    IF my_rec.stype = 'amount'
    THEN
      DELETE
        FROM merchant_statistic_bucket_amount
       WHERE bmeta_serial_id = my_rec.bmeta_serial_id
         AND bucket_start >= my_end;
    ELSE
      DELETE
        FROM merchant_statistic_bucket_counter
       WHERE bmeta_serial_id = my_rec.bmeta_serial_id
         AND bucket_start >= my_end;
    END IF;
  END LOOP;
END $$;
COMMENT ON PROCEDURE merchant_statistic_bucket_gc
  IS 'Performs garbage collection of the merchant_statistic_bucket_counter and merchant_statistic_bucket_amount tables';
DROP FUNCTION IF EXISTS merchant_statistics_bucket_end;
CREATE FUNCTION merchant_statistics_bucket_end (
  IN in_bucket_start INT8,
  IN in_range statistic_range,
  OUT out_bucket_end INT8
)
LANGUAGE plpgsql
AS $$
BEGIN
    IF in_range='quarter'
    THEN
      out_bucket_end = EXTRACT(EPOCH FROM CAST(date_trunc('quarter', to_timestamp(in_bucket_start)::date) + interval '3 months' AS date));
    ELSE
      out_bucket_end = EXTRACT(EPOCH FROM CAST(to_timestamp(in_bucket_start)::date + ('1 ' || in_range)::interval AS date));
    END IF;
END $$;
COMMENT ON FUNCTION merchant_statistics_bucket_end
IS 'computes the end time of the bucket for an event at the current time given the desired bucket range';
CREATE OR REPLACE FUNCTION handle_inventory_changes()
  RETURNS TRIGGER AS $$
DECLARE
  my_merchant_serial BIGINT;
  resolved_body TEXT;
  webhook RECORD; 
BEGIN
  my_merchant_serial := COALESCE(OLD.merchant_serial, NEW.merchant_serial);
  IF TG_OP = 'INSERT' THEN
    FOR webhook IN
      SELECT webhook_serial,
             merchant_serial,
      url,
      http_method,
      body_template
      FROM merchant.merchant_webhook
      WHERE event_type = 'inventory_added'
        AND merchant_serial = my_merchant_serial
      LOOP
        resolved_body := webhook.body_template;
        resolved_body := replace_placeholder(resolved_body,
                                             'webhook_type',
                                             'inventory_added');
        resolved_body := replace_placeholder(resolved_body,
                                             'product_serial',
                                             NEW.product_serial::TEXT);
        resolved_body := replace_placeholder(resolved_body,
                                             'product_id',
                                             NEW.product_id);
        resolved_body := replace_placeholder(resolved_body,
                                             'description',
                                             NEW.description);
        resolved_body := replace_placeholder(resolved_body,
                                             'description_i18n',
                                             NEW.description_i18n::TEXT);
        resolved_body := replace_placeholder(resolved_body,
                                             'unit',
                                             NEW.unit);
        resolved_body := replace_placeholder(resolved_body,
                                             'image',
                                             NEW.image);
        resolved_body := replace_placeholder(resolved_body,
                                             'taxes',
                                             NEW.taxes::TEXT);
        resolved_body := replace_placeholder(resolved_body,
                                             'price',
                                             NEW.price::TEXT);
        resolved_body := replace_placeholder(resolved_body,
                                             'total_stock',
                                             NEW.total_stock::TEXT);
        resolved_body := replace_placeholder(resolved_body,
                                             'total_sold',
                                             NEW.total_sold::TEXT);
        resolved_body := replace_placeholder(resolved_body,
                                             'total_lost',
                                             NEW.total_lost::TEXT);
        resolved_body := replace_placeholder(resolved_body,
                                             'address',
                                             NEW.address::TEXT);
        resolved_body := replace_placeholder(resolved_body,
                                             'next_restock',
                                             NEW.next_restock::TEXT);
        resolved_body := replace_placeholder(resolved_body,
                                             'minimum_age',
                                             NEW.minimum_age::TEXT);
        INSERT INTO merchant.merchant_pending_webhooks
        (merchant_serial, webhook_serial, url, http_method, body)
        VALUES
          (webhook.merchant_serial,
           webhook.webhook_serial,
           webhook.url,
           webhook.http_method,
           resolved_body);
      END LOOP;
    NOTIFY XXJWF6C1DCS1255RJH7GQ1EK16J8DMRSQ6K9EDKNKCP7HRVWAJPKG;
  END IF;
  IF TG_OP = 'UPDATE' THEN
    FOR webhook IN
      SELECT webhook_serial,
             merchant_serial,
      url,
      http_method,
      body_template
      FROM merchant.merchant_webhook
      WHERE event_type = 'inventory_updated'
        AND merchant_serial = my_merchant_serial
      LOOP
        resolved_body := webhook.body_template;
        resolved_body := replace_placeholder(resolved_body,
                                             'webhook_type',
                                             'inventory_updated');
        resolved_body := replace_placeholder(resolved_body,
                                             'product_serial',
                                             NEW.product_serial::TEXT);
        resolved_body := replace_placeholder(resolved_body,
                                             'product_id',
                                             NEW.product_id);
        resolved_body := replace_placeholder(resolved_body,
                                             'old_description',
                                             OLD.description);
        resolved_body := replace_placeholder(resolved_body,
                                             'description',
                                             NEW.description);
        resolved_body := replace_placeholder(resolved_body,
                                             'old_description_i18n',
                                             OLD.description_i18n::TEXT);
        resolved_body := replace_placeholder(resolved_body,
                                             'description_i18n',
                                             NEW.description_i18n::TEXT);
        resolved_body := replace_placeholder(resolved_body,
                                             'old_unit',
                                             OLD.unit);
        resolved_body := replace_placeholder(resolved_body,
                                             'unit',
                                             NEW.unit);
        resolved_body := replace_placeholder(resolved_body,
                                             'old_image',
                                             OLD.image);
        resolved_body := replace_placeholder(resolved_body,
                                             'image',
                                             NEW.image);
        resolved_body := replace_placeholder(resolved_body,
                                             'old_taxes',
                                             OLD.taxes::TEXT);
        resolved_body := replace_placeholder(resolved_body,
                                             'taxes',
                                             NEW.taxes::TEXT);
        resolved_body := replace_placeholder(resolved_body,
                                             'old_price',
                                             OLD.price::TEXT);
        resolved_body := replace_placeholder(resolved_body,
                                             'price',
                                             NEW.price::TEXT);
        resolved_body := replace_placeholder(resolved_body,
                                             'old_total_stock',
                                             OLD.total_stock::TEXT);
        resolved_body := replace_placeholder(resolved_body,
                                             'total_stock',
                                             NEW.total_stock::TEXT);
        resolved_body := replace_placeholder(resolved_body,
                                             'old_total_sold',
                                             OLD.total_sold::TEXT);
        resolved_body := replace_placeholder(resolved_body,
                                             'total_sold',
                                             NEW.total_sold::TEXT);
        resolved_body := replace_placeholder(resolved_body,
                                             'old_total_lost',
                                             OLD.total_lost::TEXT);
        resolved_body := replace_placeholder(resolved_body,
                                             'total_lost',
                                             NEW.total_lost::TEXT);
        resolved_body := replace_placeholder(resolved_body,
                                             'old_address',
                                             OLD.address::TEXT);
        resolved_body := replace_placeholder(resolved_body,
                                             'address',
                                             NEW.address::TEXT);
        resolved_body := replace_placeholder(resolved_body,
                                             'old_next_restock',
                                             OLD.next_restock::TEXT);
        resolved_body := replace_placeholder(resolved_body,
                                             'next_restock',
                                             NEW.next_restock::TEXT);
        resolved_body := replace_placeholder(resolved_body,
                                             'old_minimum_age',
                                             OLD.minimum_age::TEXT);
        resolved_body := replace_placeholder(resolved_body,
                                             'minimum_age',
                                             NEW.minimum_age::TEXT);
        INSERT INTO merchant.merchant_pending_webhooks
        (merchant_serial, webhook_serial, url, http_method, body)
        VALUES
          (webhook.merchant_serial,
           webhook.webhook_serial,
           webhook.url,
           webhook.http_method,
           resolved_body);
      END LOOP;
    NOTIFY XXJWF6C1DCS1255RJH7GQ1EK16J8DMRSQ6K9EDKNKCP7HRVWAJPKG;
  END IF;
  IF TG_OP = 'DELETE' THEN
    FOR webhook IN
      SELECT webhook_serial,
             merchant_serial,
      url,
      http_method,
      body_template
      FROM merchant.merchant_webhook
      WHERE event_type = 'inventory_deleted'
        AND merchant_serial = my_merchant_serial
      LOOP
        resolved_body := webhook.body_template;
        resolved_body := replace_placeholder(resolved_body,
                                             'webhook_type',
                                             'inventory_deleted');
        resolved_body := replace_placeholder(resolved_body,
                                             'product_serial',
                                             OLD.product_serial::TEXT);
        resolved_body := replace_placeholder(resolved_body,
                                             'product_id',
                                             OLD.product_id);
        resolved_body := replace_placeholder(resolved_body,
                                             'description',
                                             OLD.description);
        resolved_body := replace_placeholder(resolved_body,
                                             'description_i18n',
                                             OLD.description_i18n::TEXT);
        resolved_body := replace_placeholder(resolved_body,
                                             'unit',
                                             OLD.unit);
        resolved_body := replace_placeholder(resolved_body,
                                             'image',
                                             OLD.image);
        resolved_body := replace_placeholder(resolved_body,
                                             'taxes',
                                             OLD.taxes::TEXT);
        resolved_body := replace_placeholder(resolved_body,
                                             'price',
                                             OLD.price::TEXT);
        resolved_body := replace_placeholder(resolved_body,
                                             'total_stock',
                                             OLD.total_stock::TEXT);
        resolved_body := replace_placeholder(resolved_body,
                                             'total_sold',
                                             OLD.total_sold::TEXT);
        resolved_body := replace_placeholder(resolved_body,
                                             'total_lost',
                                             OLD.total_lost::TEXT);
        resolved_body := replace_placeholder(resolved_body,
                                             'address',
                                             OLD.address::TEXT);
        resolved_body := replace_placeholder(resolved_body,
                                             'next_restock',
                                             OLD.next_restock::TEXT);
        resolved_body := replace_placeholder(resolved_body,
                                             'minimum_age',
                                             OLD.minimum_age::TEXT);
        INSERT INTO merchant.merchant_pending_webhooks
        (merchant_serial, webhook_serial, url, http_method, body)
        VALUES
          (webhook.merchant_serial,
           webhook.webhook_serial,
           webhook.url,
           webhook.http_method,
           resolved_body);
      END LOOP;
    NOTIFY XXJWF6C1DCS1255RJH7GQ1EK16J8DMRSQ6K9EDKNKCP7HRVWAJPKG;
  END IF;
  RETURN NULL;
END;
$$ LANGUAGE plpgsql;
COMMENT ON FUNCTION handle_inventory_changes
  IS 'Function to handle inventory changes and notify webhooks';
CREATE OR REPLACE FUNCTION handle_category_changes()
RETURNS TRIGGER AS $$
DECLARE
  my_merchant_serial BIGINT;
  resolved_body TEXT;
  webhook RECORD; 
BEGIN
  my_merchant_serial := COALESCE(OLD.merchant_serial, NEW.merchant_serial);
  IF TG_OP = 'INSERT' THEN
    FOR webhook IN
      SELECT webhook_serial,
             merchant_serial,
      url,
      http_method,
      body_template
      FROM merchant.merchant_webhook
      WHERE event_type = 'category_added'
        AND merchant_serial = my_merchant_serial
    LOOP
      resolved_body := webhook.body_template;
      resolved_body := replace_placeholder(resolved_body,
                                           'webhook_type',
                                           'category_added');
      resolved_body := replace_placeholder(resolved_body,
                                           'category_serial',
                                           NEW.category_serial::TEXT);
      resolved_body := replace_placeholder(resolved_body,
                                           'category_name',
                                           NEW.category_name);
      resolved_body := replace_placeholder(resolved_body,
                                           'merchant_serial',
                                           my_merchant_serial::TEXT);
      INSERT INTO merchant.merchant_pending_webhooks
      (merchant_serial, webhook_serial, url, http_method, body)
      VALUES
      (webhook.merchant_serial,
       webhook.webhook_serial,
       webhook.url,
       webhook.http_method,
       resolved_body);
    END LOOP;
    NOTIFY XXJWF6C1DCS1255RJH7GQ1EK16J8DMRSQ6K9EDKNKCP7HRVWAJPKG;
  END IF;
  IF TG_OP = 'UPDATE' THEN
    FOR webhook IN
      SELECT webhook_serial,
             merchant_serial,
      url,
      http_method,
      body_template
      FROM merchant.merchant_webhook
      WHERE event_type = 'category_updated'
        AND merchant_serial = my_merchant_serial
    LOOP
      resolved_body := webhook.body_template;
      resolved_body := replace_placeholder(resolved_body,
                                           'webhook_type',
                                           'category_updated');
      resolved_body := replace_placeholder(resolved_body,
                                           'category_serial',
                                           NEW.category_serial::TEXT);
      resolved_body := replace_placeholder(resolved_body,
                                           'old_category_name',
                                           OLD.category_name);
      resolved_body := replace_placeholder(resolved_body,
                                           'category_name',
                                           NEW.category_name);
      resolved_body := replace_placeholder(resolved_body,
                                           'category_name_i18n',
                                           NEW.category_name_i18n::TEXT);
      resolved_body := replace_placeholder(resolved_body,
                                           'old_category_name_i18n',
                                           OLD.category_name_i18n::TEXT);
      INSERT INTO merchant.merchant_pending_webhooks
        (merchant_serial, webhook_serial, url, http_method, body)
      VALUES
        (webhook.merchant_serial,
         webhook.webhook_serial,
         webhook.url,
         webhook.http_method,
         resolved_body);
    END LOOP;
    NOTIFY XXJWF6C1DCS1255RJH7GQ1EK16J8DMRSQ6K9EDKNKCP7HRVWAJPKG;
  END IF;
  IF TG_OP = 'DELETE' THEN
    FOR webhook IN
      SELECT webhook_serial,
             merchant_serial,
      url,
      http_method,
      body_template
      FROM merchant.merchant_webhook
      WHERE event_type = 'category_deleted'
        AND merchant_serial = my_merchant_serial
    LOOP
      resolved_body := webhook.body_template;
      resolved_body := replace_placeholder(resolved_body,
                                           'webhook_type',
                                           'category_deleted');
      resolved_body := replace_placeholder(resolved_body,
                                           'category_serial',
                                           OLD.category_serial::TEXT);
      resolved_body := replace_placeholder(resolved_body,
                                           'category_name',
                                           OLD.category_name);
      INSERT INTO merchant.merchant_pending_webhooks
      (merchant_serial, webhook_serial, url, http_method, body)
      VALUES
      (webhook.merchant_serial,
       webhook.webhook_serial,
       webhook.url,
       webhook.http_method,
       resolved_body);
    END LOOP;
    NOTIFY XXJWF6C1DCS1255RJH7GQ1EK16J8DMRSQ6K9EDKNKCP7HRVWAJPKG;
  END IF;
  RETURN NULL;
END;
$$ LANGUAGE plpgsql;
COMMENT ON FUNCTION handle_category_changes
  IS 'Trigger function to handle pending webhooks for category changes';
DROP PROCEDURE IF EXISTS merchant_do_gc;
CREATE PROCEDURE merchant_do_gc(in_now INT8)
LANGUAGE plpgsql
AS $$
BEGIN
  DELETE FROM merchant_instances
    WHERE validation_needed
      AND validation_expiration < in_now;
  CALL merchant_statistic_amount_gc ();
  CALL merchant_statistic_bucket_gc ();
  CALL merchant_statistic_counter_gc ();
  DELETE FROM tan_challenges
    WHERE expiration_date < in_now;
END $$;
COMMENT ON PROCEDURE merchant_do_gc
  IS 'calls all other garbage collection subroutines';
COMMIT;
